Skip to content

chore: sync with upstream 2026-07-27 - #85

Merged
NicolasWalter merged 7 commits into
mainfrom
sync/upstream-2026-07-27
Jul 27, 2026
Merged

chore: sync with upstream 2026-07-27#85
NicolasWalter merged 7 commits into
mainfrom
sync/upstream-2026-07-27

Conversation

@NicolasWalter

@NicolasWalter NicolasWalter commented Jul 27, 2026

Copy link
Copy Markdown

Automated upstream sync

Clean merge from ColeMurray/background-agents@main.

This PR was opened automatically by .github/workflows/sync-upstream.yml. Review the commit list and merge when CI is green.

Summary by CodeRabbit

  • New Features

    • Added Better Auth browser sign-in with GitHub and optional Google support.
    • Added secure browser-auth proxying, session handling, admission controls, and origin-bound local development support.
    • Added safer validation for classifier and control-plane responses.
  • Bug Fixes

    • Prevented incorrect Content-Length and encoding headers when serving decoded media and attachments.
    • Improved authentication error handling and retired legacy session-token routes.
  • Documentation

    • Updated setup, deployment, environment, and browser-auth guidance for the new authentication flow.
  • Chores

    • Extended integration-test timeout to improve CI reliability.

ColeMurray and others added 7 commits July 26, 2026 00:36
## Summary

- add CP-owned admission policy with existing OR semantics,
deny-by-default behavior, complete verified-email evaluation, and
same-flow GitHub organization membership checks
- resolve browser identities only by canonical issuer plus exact
immutable provider subject, with provenance-bearing verified-email
reservations and explicit account-link-required collisions
- orchestrate consumed provider callbacks through provider-specific
callback handlers, admission, identity resolution, credential capture,
and exact client-bound authorization-code issuance
- separate browser sign-in policy from persistence: the resolver owns
evidence, collision, and retry policy while the DB store owns row
decoding and atomic multi-table writes
- add a GitHub App permission preflight that verifies both registration
and installation permissions before cutover

## Security properties

- revalidates the persisted client and exact redirect URI after
single-use state consumption and before provider exchange
- isolates provider-specific callback mechanics (including Google's OIDC
nonce binding) behind an exhaustive provider-handler registry; adding
Okta requires an explicit handler instead of falling through to another
provider
- keeps Google credentials out of storage and uses the maintained
provider adapters from ColeMurray#1118 for OAuth and OIDC protocol validation
- preserves provider subjects exactly; identity remains the
case-sensitive `(issuer, subject)` tuple
- never reparents an established immutable subject based on email
- creates each new user, issuer-qualified identity, verified-email
claims, and GitHub provider credential in one atomic D1 batch
- refreshes existing identity metadata, claims, and versioned provider
credentials in one retryable D1 batch
- bounds verified-email evidence and uses JSON-table statements so a
valid GitHub result cannot exceed D1's 100-bind limit or require one
statement per email
- retries uniqueness races by re-reading issuer-subject and claim
ownership; it never falls back to an email owner
- preserves legacy canonical-email claim provenance while allowing
current provider claims to advance verification timestamps
- maps provider, admission, and collision failures to bounded callback
errors without retaining OAuth state, raw causes, tokens, provider
bodies, emails, subjects, or authorization codes
- makes GitHub `email_addresses: read` an intrinsic preflight
requirement because sign-in always reads `/user/emails`

## Scope

This PR adds inert control-plane domain services and tests only. It does
not expose routes, change cookies, alter deployment configuration, or
activate the new authentication path. HTTP parsing, rate limiting,
telemetry, composition, and cutover wiring remain follow-up work.

Account linking remains intentionally deferred. A new subject that
collides with another trusted email reservation fails with
`account_link_required` and creates nothing.

## TDD and validation

Red-green-refactor coverage includes admission parity, provider-mixup
rejection, exact redirect rebinding, provider-handler dispatch, bounded
and sanitized provider failures, exact subject preservation, immutable
identity races, bounded D1-safe email fan-out, schema-pinned
stale-credential retry and atomic rollback, email collisions, legacy
claim provenance, atomic credential capture and rollback, Google
credential rejection, and independent App/installation permission and
suspension checks.

The real D1 integration path proves provider callback -> canonical user
and credential -> authorization code -> browser session redemption and
authentication.

- `npm test -w @open-inspect/control-plane` (141 files, 2,174 tests)
- `npm run test:integration -w @open-inspect/control-plane` (61 files,
714 tests)
- `npm run typecheck`
- `npm run lint -w @open-inspect/control-plane`
- `npm run format:check`
- `npm run build -w @open-inspect/shared`
- `npm run build -w @open-inspect/control-plane`
- `git diff --check`


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added configurable sign-in admission policies (email, domain, GitHub
user, and optional GitHub organization checks) with “unsafe allow all”
behavior.
* Added GitHub App permission preflight validation for app JWT +
installation permission readiness.
* Implemented OAuth callback handling for both GitHub and Google,
including correct success/denial redirects.
* Strengthened browser sign-in identity resolution and verified-email
handling with account-linking safeguards.
* **Bug Fixes**
* Improved denial/unavailable vs server error classification, redirect
safety, and failure rollback behavior.
* **Tests**
* Expanded unit and integration coverage across admission, OAuth
callbacks, identity persistence, and credential concurrency/version
conflicts.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary

- increase the control-plane integration job timeout from five to ten
minutes
- leave every test command and all other CI limits unchanged

## Why

The complete integration suite now runs close to five minutes after
dependency installation and workerd setup. A production validation run
reached the existing job limit and GitHub cancelled the still-running
test process without a failing assertion. Ten minutes preserves a finite
bound while allowing the suite to report its actual result.

## Impact

This changes CI scheduling only. It does not change application code,
deployment behavior, or test semantics.

## Validation

- Prettier check passed for the workflow
- git diff --check passed
- the full control-plane integration suite passed locally before this
timeout adjustment

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Chores**
* Increased the integration test workflow timeout to reduce failures
caused by slow test runs.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary

- pin Better Auth 1.6.25 in the control plane
- add the minimal control-plane-owned browser-auth configuration
boundary
- make Better Auth generate canonical Open Inspect user IDs
- add the additive Better Auth user, account, session, and verification
schema
- enforce uniqueness for immutable provider identities
- make the pinned runtime validate the complete static schema contract
in workerd with real D1

## Why

This is the inert foundation for replacing the custom browser OAuth
runtime with the Better Auth implementation already validated in
production. No routes call this configuration yet, so this PR does not
change current browser authentication behavior.

The schema is intentionally browser-specific and additive. It does not
predict CLI, PAT, MFA, magic-link, or future Okta storage. Implicit
account linking is disabled; explicit linking remains follow-up work.
Before routes activate, Better Auth user IDs are projected unchanged
into canonical users.id and auth_accounts becomes the browser-provider
credential authority; the legacy credential table is not dual-written.

## Rollout

The CI timeout prerequisite in ColeMurray#1124 has merged. This branch is rebased
directly on current main and contains only the Better Auth runtime and
schema foundation.

## Validation

- control-plane lint passed
- control-plane typecheck passed
- control-plane build passed
- 141 control-plane unit files, 2,175 tests passed
- 62 control-plane integration files, 716 tests passed
- focused Better Auth workerd/D1 integration: 3 tests passed
- schema contract checks cover pinned-runtime drift, every column shape,
foreign keys, and the custom provider-identity index
- git diff --check passed

## Dependency audit

The production audit reports the existing public-main
Next.js/PostCSS/sharp advisories. Better Auth adds another dependency
path to the same installed Next version; it does not introduce a new
advisory or a second vulnerable package version.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Enabled browser authentication for the control plane with secure,
fixed cookie behavior.
* Added the core authentication database schema for users, sessions,
linked accounts, and verification records.
* Added an API behavior for anonymous session checks (returns `200` with
`null` session).
* **Bug Fixes**
* Enforced provider/account uniqueness, email uniqueness, and cascading
cleanup for user-related auth records.
* **Tests**
* Added integration coverage to validate the browser-auth runtime schema
and request/session boundary behavior.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
)

## Summary

Completes the browser-authentication cutover that began with ColeMurray#1125.

The control plane now owns Better Auth, provider verification,
admission, canonical user
projection, sessions, and provider credentials. The Next.js app is a
framework-free BFF that
forwards an exact auth-route allowlist and authenticates application
requests with both the web
service's `sig1` channel and the browser's opaque session cookie.

This is intentionally one complete cutover PR. The implementation has
already been deployed and
exercised in the production repository, including the production fixes
discovered during that
rehearsal. Merging it as one unit avoids leaving public `main` with
overlapping Better Auth,
NextAuth, bearer-token, and hand-rolled OAuth architectures.

## Resulting architecture

1. The browser calls the web application's `/api/auth/*` routes.
2. The web BFF forwards only the shared exact Better Auth route
allowlist.
3. Every forwarded request is body-bound and signed as `service:web`
with `sig1`.
4. The control plane verifies GitHub or Google identity evidence and
applies admission policy.
5. Better Auth owns browser users, provider accounts, OAuth credentials,
and opaque sessions in D1.
6. Better Auth user IDs are projected unchanged into canonical
`users.id`.
7. Browser resource requests require both:
   - a valid `service:web` channel; and
   - a valid Better Auth session cookie.
8. Authorization receives `{ kind: "user", userId }`; provider
provenance remains in the
   authentication context.
9. GitHub access and refresh remain Better Auth-owned. Session
creation/prompt flows request a
current access token through Better Auth and never copy the long-lived
refresh token.

## Review guide

The commits remain logically ordered so this large PR can be reviewed as
vertical slices:

1. **Provider identity and admission**
- defensive GitHub response validation and bounded pagination/retry
behavior;
- Google ID-token verification using Better Auth's provider
implementation;
- verified-email, GitHub-user, email-domain, and GitHub-organization
admission.
2. **Signed auth surface and BFF proxy**
   - one shared exact route allowlist;
   - control-plane `sig1` enforcement;
- transparent callback, cookie, redirect, and decoded-body header
handling.
3. **Canonical identity and compound authentication**
   - canonical ID generation and user projection;
   - provider provenance separated from the authorized principal;
   - browser session plus web-channel validation.
4. **Web cutover and provider credentials**
   - Better Auth sign-in, sign-out, and session consumption;
   - authenticated BFF resource requests;
   - Better Auth-owned GitHub credential refresh and attribution.
5. **Deployment and legacy removal**
   - control-plane provider/auth configuration;
- removal of NextAuth, web bearer tokens, and the superseded custom
OAuth server;
   - provider secrets and admission policy removed from the web runtime.
6. **Production-rehearsal fixes**
   - stale response encoding/length headers;
   - bound Workers `fetch`;
   - legacy-user collision cleanup and account backfill;
   - multi-provider session handling;
   - workerd-compatible package resolution;
   - explicit rate limiting and trusted client-IP propagation.

The final reconciliation commits remove custom OAuth files that were
merged into public `main`
after the production validation branch was created, remove the unused
direct OAuth client
dependency, and retain the schema-alignment and duration-unit
improvements from ColeMurray#1125. The
latest organization pass groups user and service authentication by
responsibility, moves GitHub
credential authority into source control, and narrows Better Auth
dependencies at both boundaries.

## Migration and deployment behavior

- `0049_backfill_better_auth_accounts.sql` idempotently seeds Better
Auth users and immutable
  GitHub/Google accounts from the existing canonical model.
- The backfill removes only partial Better Auth identity graphs left by
the accepted non-atomic D1
adapter behavior when an existing canonical email caused projection to
fail.
- Existing provider access and refresh tokens are not copied. The next
successful sign-in captures
  fresh Better Auth-owned credentials.
- Existing browser sessions are intentionally invalidated; users sign in
again after cutover.
- The existing Terraform `nextauth_secret` input and Actions
`NEXTAUTH_SECRET` name are retained as
operator-facing compatibility names, but now supply control-plane
`BROWSER_AUTH_SECRET`.
- GitHub and Google callback URLs remain on the browser-visible web
origin:
  - `<web-origin>/api/auth/callback/github`
  - `<web-origin>/api/auth/callback/google`
- Legacy custom-auth D1 tables remain additive residue for now, but no
runtime route or service
  consumes them.

For existing Cloudflare web deployments, the uploader no longer sends
OAuth or NextAuth secrets,
but Wrangler preserves already-uploaded secrets. Operators should delete
stale
`GITHUB_CLIENT_SECRET`, `GOOGLE_CLIENT_SECRET`, and `NEXTAUTH_SECRET`
values from the web Worker
after validating the cutover.

## Security properties

- Provider identities are immutable `(provider, subject)` accounts.
- GitHub and Google successful responses are runtime-validated before
admission.
- Implicit account linking is disabled.
- OAuth provider secrets and admission allowlists exist only in the
control plane.
- Browser resource routes have no service-credential-to-user fallback.
- The BFF forwards only the opaque Better Auth session cookie and
rejects malformed or duplicate
  session-cookie data.
- Auth proxy and resource request bodies are covered by `sig1`.
- Production cookies are secure, HTTP-only, host-only, and
`SameSite=Lax`; insecure cookies are
  limited to exact loopback HTTP development origins.
- Retired bearer-token and provider-identity endpoints are no longer
routed.

## Accepted scope decisions

- Better Auth's exact-pinned D1 adapter does not provide an interactive
transaction. Initial
sign-in can therefore leave recoverable partial state if a later write
fails. This rollout
accepts that risk; the migration repairs the known legacy-email
collision.
- Explicit account linking is deferred. The canonical model supports a
future linking operation,
  but implicit email linking remains disabled.
- Better Auth's in-memory limiter is best-effort per Worker isolate, not
a global abuse-control
  system.
- Better Auth stores its random session identifier in D1 and
authenticates the browser cookie with
`BROWSER_AUTH_SECRET`; this is not the literal hash-at-rest
representation from the superseded
  custom design.

## Validation

- Control-plane focused auth unit tests: **23 files, 207 tests passed**
- Control-plane focused workerd/D1 integration tests: **4 files, 28
tests passed**
- Control-plane unit tests: **136 files, 2,111 tests passed**
- Control-plane integration tests: **57 files, 653 tests passed**
- Web tests: **101 files, 790 tests passed**
- Shared tests: **36 files, 489 tests passed**
- GitHub bot tests: **7 files, 128 tests passed**
- Slack bot tests: **28 files, 338 tests passed**
- Linear bot tests: **13 files, 185 tests passed**
- Full TypeScript typecheck: passed
- Full ESLint: passed
- Full workspace build, including the production web build: passed
- Terraform recursive formatting check: passed
- Terraform initialization and validation: passed
- Thermo-nuclear maintainability review: passed with scoped cleanup
applied


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Rolled out control-plane–driven browser authentication (GitHub, with
optional Google) and a secure browser-auth proxy for web requests,
enabling authenticated enrichment for session-related flows.
* **Bug Fixes**
* Prevented forwarding stale `Content-Length`/`Content-Encoding` when
returning decoded streamed content (media/attachments/session diffs).
* Improved access-denied messaging to consistently handle both legacy
and new denial codes.
* **Documentation**
* Updated setup/deployment guides and environment examples to use the
new browser-auth secret terminology and configuration expectations.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This is an automated nightly unsafe-cast remediation sweep. It replaces
selected high-risk Linear bot boundary assertions with
parse-don't-assert validation, following the TypeScript Coding Standards
for unsafe casts and the Zod boundary-validation pattern established in
PR ColeMurray#807.

| Finding | Risk | Cast removed | Fix |
| --- | --- | --- | --- |
| `packages/linear-bot/src/webhook-handler.ts:177` | HIGH |
Control-plane create-session response cast to `{ sessionId: string }`,
bypassing the existing shared schema | Reused
`createSessionResponseSchema.safeParse`; malformed success responses now
follow the existing create-session failure path |
| `packages/linear-bot/src/webhook-handler.ts:380` | HIGH |
Control-plane session events response cast to `Array<{ type; data }>`
feeding follow-up prompt context | Added a local Zod response schema for
the consumed event fields and skip prior-context enrichment when parsing
fails |
| `packages/linear-bot/src/utils/integration-config.ts:58` | HIGH |
Control-plane resolved Linear config response cast to
`ResolvedLinearConfig` | Added a package-local Zod schema and made
`z.infer` the config type source of truth; malformed responses fall back
to defaults |

Verification:

| Command | Result |
| --- | --- |
| `npm run build -w @open-inspect/shared` | Passed |
| `npm run build -w @open-inspect/linear-bot` | Passed |
| `npm run typecheck` | Passed |
| `npm test -w @open-inspect/linear-bot` | Passed, 13 files / 189 tests
|
| `npm run lint -w @open-inspect/linear-bot` | Passed |
| `npm run lint -- --ignore-pattern '.opencode/**'` | Passed for tracked
repo code; literal `npm run lint` in this workspace is blocked by a
pre-existing untracked `.opencode/` directory that is not part of this
PR |
| `npm run format` | Passed |

---
*Created with
[Open-Inspect](https://open-inspect-prod.vercel.app/session/feca6455e6cb748e1ada460d645a4c87)*

---------

Co-authored-by: OpenInspect <open-inspect@noreply.github.com>
Co-authored-by: Cole Murray <colemurray.cs@gmail.com>
This is an automated nightly unsafe-cast remediation. It replaces unsafe
assertions at an external Anthropic API boundary with package-local Zod
validation, following the TypeScript Coding Standards unsafe-cast /
parse-don't-assert guidance and the Zod boundary-validation pattern
established in PR ColeMurray#807.

| Finding | Risk | Cast removed | Fix |
| --- | --- | --- | --- |
| `packages/linear-bot/src/classifier/index.ts:141` | HIGH | `(await
response.json()) as AnthropicResponse` on an external API response |
Added `anthropicMessagesResponseSchema` and `safeParse` before reading
`content` |
| `packages/linear-bot/src/classifier/index.ts:148` | HIGH |
`toolBlock.input as Record<string, unknown>` plus `input.confidence as
ConfidenceLevel` | Added `classifyToolInputSchema` with `repoId` modeled
as `string | null`, confidence enum validation, and `safeParse` before
returning the `z.infer` type |

Verification:

| Command | Result |
| --- | --- |
| `npm run build -w @open-inspect/shared` | Passed |
| `npm run build -w @open-inspect/linear-bot` | Passed |
| `npm test -w @open-inspect/linear-bot` | Passed, 200 tests |
| `npm run typecheck` | Passed |
| `npm run lint -w @open-inspect/linear-bot` | Passed |
| `npm run format` | Passed |
| `npm run lint` | Passed in a clean worktree for this commit; the
active agent workspace contains untracked local `.opencode` tool files
that are not part of this PR and cause root lint false positives there |

---
*Created with
[Open-Inspect](https://open-inspect-prod.vercel.app/session/8c9388c89bcb8c9de06d4b25235c0809)*

Co-authored-by: waclaude <colemurray.cs+ghwaclaude@gmail.com>
@github-actions

Copy link
Copy Markdown

Terraform Validation Results

Step Status
Format
Init
Validate

Pushed by: @NicolasWalter, Action: pull_request

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR replaces web-owned NextAuth and opaque session tokens with Better Auth browser sessions, signed web-service requests, control-plane admission, provider enrichment, new auth persistence, and updated web/deployment configuration. It also adds response validation and removes retired authentication routes.

Changes

Browser authentication cutover

Layer / File(s) Summary
Authentication foundation
packages/control-plane/src/auth/*
Adds Better Auth session verification, signed service-request authentication, authentication provenance, and simplified user principals.
Provider and persistence integration
packages/control-plane/src/auth/user/*, packages/control-plane/src/db/*, terraform/d1/migrations/*
Adds provider profile resolution, admission policies, canonical user projection, Better Auth tables, and legacy account backfill.
Control-plane routing
packages/control-plane/src/router.ts, packages/control-plane/src/routes/*, packages/shared/src/browser-auth-routes.ts
Adds allowlisted browser-auth proxy routes and requires both the web service signature and browser session for browser requests.
Web authentication BFF
packages/web/src/lib/*, packages/web/src/app/api/auth/*, packages/web/src/app/providers.tsx
Replaces NextAuth client/server boundaries with Better Auth session fetching, cookie serialization, signed control-plane requests, and proxy handlers.
Web API forwarding
packages/web/src/app/api/sessions/*, packages/web/src/app/api/automations/*
Forwards display-only auth fields, uses canonical session user IDs, and omits invalid encoded-body headers.
Validation and deployment support
packages/linear-bot/*, terraform/*, docs/*, .github/workflows/*
Adds Zod response validation, updates Better Auth secrets and bindings, documents origin-bound browser auth, and extends CI integration timeout.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested reviewers: colemurray

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is generic and only says the branch was synced upstream, without describing the substantive changes. Use a concise title that names the main change, e.g. "chore: migrate control plane to Better Auth browser sessions".
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Trivy (0.72.0)

Trivy execution failed: 2026-07-27T06:22:30Z FATAL Fatal error run error: fs scan error: scan error: scan failed: failed analysis: post analysis error: post analysis error: kubernetes scan error: fs filter error: fs filter error: walk error range error: stat doctor.config.json: no such file or directory: range error: stat doctor.config.json: no such file or directory


Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown

Terraform Plan Results

Status: ✅ Success

Show Plan
terraform_data.cloudflare_custom_domain_gate: Refreshing state... [id=fa456fac-6c14-16e4-a484-3338d1a3718d]
terraform_data.access_control_gate: Refreshing state... [id=841ab6bc-98a7-018c-1031-ebad4f8b62bc]
data.external.modal_source_hash[0]: Reading...
null_resource.github_bot_build[0]: Refreshing state... [id=8669455087823957968]
null_resource.linear_bot_build[0]: Refreshing state... [id=5896821814655107141]
null_resource.slack_bot_build[0]: Refreshing state... [id=3828387022104391843]
random_password.service_auth_secret_linear_bot: Refreshing state... [id=none]
random_password.service_auth_secret_web: Refreshing state... [id=none]
random_password.service_auth_secret_github_bot: Refreshing state... [id=none]
random_password.service_auth_secret_slack_bot: Refreshing state... [id=none]
random_password.service_auth_secret_modal: Refreshing state... [id=none]
random_password.image_callback_token_pepper: Refreshing state... [id=none]
null_resource.web_app_cloudflare_build[0]: Refreshing state... [id=5412068031001148538]
null_resource.control_plane_build: Refreshing state... [id=8869648852438369746]
local_file.web_app_wrangler_production[0]: Refreshing state... [id=f3ffb89a3b5665e9d54b7790151b3b4b01b357f7]
module.linear_kv[0].cloudflare_workers_kv_namespace.this: Refreshing state... [id=d003f1ad81384910a1f48a0a33f18c09]
cloudflare_d1_database.main: Refreshing state... [id=dba95b03-ace9-47a8-81e9-6e39d8d694c5]
cloudflare_r2_bucket.media: Refreshing state... [id=open-inspect-media-primo]
module.github_kv[0].cloudflare_workers_kv_namespace.this: Refreshing state... [id=87dbfaa1d9ce4a37a42e04c57c434a72]
cloudflare_queue.slack_completion_delivery_dlq[0]: Refreshing state... [id=06ce03d2663f4aea937b0c0c1c379c17]
module.slack_kv[0].cloudflare_workers_kv_namespace.this: Refreshing state... [id=729b357dbb5e4c9d99ec9212cc45766e]
module.session_index_kv.cloudflare_workers_kv_namespace.this: Refreshing state... [id=7f18644fbed34121bbe3a196f373ea93]
cloudflare_queue.slack_completion_delivery[0]: Refreshing state... [id=56ef0f3e13bd46a3a39f30c79ec547fa]
module.modal_app[0].null_resource.modal_secrets[0]: Refreshing state... [id=2786381799123975256]
data.external.modal_source_hash[0]: Read complete after 0s [id=-]
module.modal_app[0].null_resource.modal_deploy: Refreshing state... [id=7095165949869290970]
module.slack_bot_worker[0].cloudflare_worker.this: Refreshing state... [id=5200e96d69804ea296e1f3a6b39e4243]
module.linear_bot_worker[0].cloudflare_worker.this: Refreshing state... [id=33782d80e8ff4af9b30b92870084b674]
null_resource.d1_migrations: Refreshing state... [id=7966128981626684664]
module.linear_bot_worker[0].cloudflare_worker_version.this: Refreshing state... [id=16b53281-fe2f-4885-a89e-c2239de3b811]
module.slack_bot_worker[0].cloudflare_worker_version.this: Refreshing state... [id=6551b217-d1f4-4f1d-8072-253ad7a0d163]
module.linear_bot_worker[0].cloudflare_workers_deployment.this: Refreshing state... [id=5dc80780-d35d-46b2-8483-290d83a77ea2]
module.slack_bot_worker[0].cloudflare_workers_deployment.this: Refreshing state... [id=21642fd7-f856-48de-a26d-d501d8817102]
module.control_plane_worker.cloudflare_worker.this: Refreshing state... [id=c208a60c393e45e38eb502346bb7ce1e]
cloudflare_queue_consumer.slack_completion_delivery[0]: Refreshing state...
module.control_plane_worker.cloudflare_worker_version.this: Refreshing state... [id=5a41bfd5-5d1b-4f9e-a282-9235a9ad3bd9]
module.control_plane_worker.cloudflare_workers_deployment.this: Refreshing state... [id=6c613670-4aa5-41bd-bb95-206aee4ccd49]
module.control_plane_worker.cloudflare_workers_cron_trigger.this[0]: Refreshing state... [id=open-inspect-control-plane-primo]
null_resource.web_app_cloudflare_deploy[0]: Refreshing state... [id=7223362243013327446]
module.github_bot_worker[0].cloudflare_worker.this: Refreshing state... [id=4b5e2696491a41eaaa124f4e2a9855f2]
null_resource.web_app_cloudflare_secrets[0]: Refreshing state... [id=2052594936711735049]
module.github_bot_worker[0].cloudflare_worker_version.this: Refreshing state... [id=7aaed149-c286-4edb-9ace-310ce4d3ccd1]
module.github_bot_worker[0].cloudflare_workers_deployment.this: Refreshing state... [id=2129eb15-5cf8-47a9-9f96-ea9e5ddae4e6]

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create
  ~ update in-place
-/+ destroy and then create replacement

Terraform will perform the following actions:

  # local_file.web_app_wrangler_production[0] will be created
  + resource "local_file" "web_app_wrangler_production" {
      + content              = (sensitive value)
      + content_base64sha256 = (known after apply)
      + content_base64sha512 = (known after apply)
      + content_md5          = (known after apply)
      + content_sha1         = (known after apply)
      + content_sha256       = (known after apply)
      + content_sha512       = (known after apply)
      + directory_permission = "0777"
      + file_permission      = "0777"
      + filename             = "../../..//packages/web/wrangler.production.toml"
      + id                   = (known after apply)
    }

  # null_resource.control_plane_build must be replaced
-/+ resource "null_resource" "control_plane_build" {
      ~ id       = "8869648852438369746" -> (known after apply)
      ~ triggers = { # forces replacement
          ~ "always_run" = "2026-07-26T06:18:24Z" -> (known after apply)
        }
    }

  # null_resource.d1_migrations must be replaced
-/+ resource "null_resource" "d1_migrations" {
      ~ id       = "7966128981626684664" -> (known after apply)
      ~ triggers = { # forces replacement
          ~ "migrations_sha" = "803b74bf318acf1260ff63e62011811a74b0a1bdab14c52ae8bfe06f55bfff9f" -> "c208bd784688703e385cfeb4b4d09724f1ad2119d0ef2d70dc15c37a2a51bdf6"
            # (1 unchanged element hidden)
        }
    }

  # null_resource.github_bot_build[0] must be replaced
-/+ resource "null_resource" "github_bot_build" {
      ~ id       = "8669455087823957968" -> (known after apply)
      ~ triggers = { # forces replacement
          ~ "always_run" = "2026-07-26T06:18:24Z" -> (known after apply)
        }
    }

  # null_resource.linear_bot_build[0] must be replaced
-/+ resource "null_resource" "linear_bot_build" {
      ~ id       = "5896821814655107141" -> (known after apply)
      ~ triggers = { # forces replacement
          ~ "always_run" = "2026-07-26T06:18:24Z" -> (known after apply)
        }
    }

  # null_resource.slack_bot_build[0] must be replaced
-/+ resource "null_resource" "slack_bot_build" {
      ~ id       = "3828387022104391843" -> (known after apply)
      ~ triggers = { # forces replacement
          ~ "always_run" = "2026-07-26T06:18:24Z" -> (known after apply)
        }
    }

  # null_resource.web_app_cloudflare_build[0] must be replaced
-/+ resource "null_resource" "web_app_cloudflare_build" {
      ~ id       = "5412068031001148538" -> (known after apply)
      ~ triggers = { # forces replacement
          ~ "always_run" = "2026-07-26T06:18:24Z" -> (known after apply)
        }
    }

  # null_resource.web_app_cloudflare_deploy[0] must be replaced
-/+ resource "null_resource" "web_app_cloudflare_deploy" {
      ~ id       = "7223362243013327446" -> (known after apply)
      ~ triggers = { # forces replacement
          ~ "always_run" = "2026-07-26T06:19:07Z" -> (known after apply)
        }
    }

  # null_resource.web_app_cloudflare_secrets[0] must be replaced
-/+ resource "null_resource" "web_app_cloudflare_secrets" {
      ~ id       = "2052594936711735049" -> (known after apply)
      ~ triggers = { # forces replacement
          ~ "secrets_hash" = (sensitive value)
        }
    }

  # module.control_plane_worker.cloudflare_worker.this will be updated in-place
  ~ resource "cloudflare_worker" "this" {
        id             = "c208a60c393e45e38eb502346bb7ce1e"
        name           = "open-inspect-control-plane-primo"
      ~ observability  = {
          ~ logs               = {
              + destinations       = (known after apply)
                # (4 unchanged attributes hidden)
            }
          ~ traces             = {
              + destinations       = (known after apply)
                # (3 unchanged attributes hidden)
            }
            # (2 unchanged attributes hidden)
        }
      ~ references     = {
          ~ dispatch_namespace_outbounds = [] -> (known after apply)
          ~ domains                      = [] -> (known after apply)
          ~ durable_objects              = [
              - {
                  - namespace_id   = "4c77239db3614a6aac69a90e1fbd8955" -> null
                  - namespace_name = "open-inspect-control-plane-primo_SessionDO" -> null
                  - worker_id      = "c208a60c393e45e38eb502346bb7ce1e" -> null
                  - worker_name    = "open-inspect-control-plane-primo" -> null
                },
              - {
                  - namespace_id   = "bf3328c8ebcb4039855ed7fcca6eb7e9" -> null
                  - namespace_name = "open-inspect-control-plane-primo_SchedulerDO" -> null
                  - worker_id      = "c208a60c393e45e38eb502346bb7ce1e" -> null
                  - worker_name    = "open-inspect-control-plane-primo" -> null
                },
            ] -> (known after apply)
          ~ queues                       = [] -> (known after apply)
          ~ workers                      = [
              - {
                  - id   = "33782d80e8ff4af9b30b92870084b674" -> null
                  - name = "open-inspect-linear-bot-primo" -> null
                },
              - {
                  - id   = "5200e96d69804ea296e1f3a6b39e4243" -> null
                  - name = "open-inspect-slack-bot-primo" -> null
                },
              - {
                  - id   = "ac07332f8b0f4cdfa4ca04f966a9fa61" -> null
                  - name = "open-inspect-web-primo" -> null
                },
              - {
                  - id   = "4b5e2696491a41eaaa124f4e2a9855f2" -> null
                  - name = "open-inspect-github-bot-primo" -> null
                },
            ] -> (known after apply)
        } -> (known after apply)
        tags           = []
      ~ updated_on     = "2026-07-26T06:18:33Z" -> (known after apply)
        # (6 unchanged attributes hidden)
    }

  # module.control_plane_worker.cloudflare_worker_version.this must be replaced
-/+ resource "cloudflare_worker_version" "this" {
      ~ annotations         = {
          + workers_message      = (known after apply)
          + workers_tag          = (known after apply)
          ~ workers_triggered_by = "create_version_api" -> (known after apply)
        } -> (known after apply)
      ~ bindings            = (sensitive value) # forces replacement
      ~ created_on          = "2026-07-26T06:18:35Z" -> (known after apply)
      ~ id                  = "5a41bfd5-5d1b-4f9e-a282-9235a9ad3bd9" -> (known after apply)
      + limits              = (known after apply)
      + main_script_base64  = (known after apply)
      ~ migration_tag       = "v1" -> (known after apply)
      ~ modules             = [
          - { # forces replacement
              - content_file   = "../../..//packages/control-plane/dist/index.js" -> null
              - content_sha256 = "c6feabd617fe14ecb501d6cf953e0b11e6e785a4fec6006bebce7a9891a2ee04" -> null
              - content_type   = "application/javascript+module" -> null
              - name           = "index.js" -> null
            },
          + { # forces replacement
              + content_file   = "../../..//packages/control-plane/dist/index.js"
              + content_sha256 = "717bc1123d997eca0a615c06ec1e22029fde019030e947b67faa1a829e4828f0"
              + content_type   = "application/javascript+module"
              + name           = "index.js"
            },
        ]
      ~ number              = 57 -> (known after apply)
      ~ source              = "terraform" -> (known after apply)
      ~ startup_time_ms     = 45 -> (known after apply)
      ~ urls                = [] -> (known after apply)
        # (6 unchanged attributes hidden)
    }

  # module.control_plane_worker.cloudflare_workers_deployment.this must be replaced
-/+ resource "cloudflare_workers_deployment" "this" {
      ~ annotations  = {
          + workers_message      = (known after apply)
          ~ workers_triggered_by = "deployment" -> (known after apply)
        } -> (known after apply)
      ~ author_email = "alejo@primo.la" -> (known after apply)
      ~ created_on   = "2026-07-26T06:18:36Z" -> (known after apply)
      ~ id           = "6c613670-4aa5-41bd-bb95-206aee4ccd49" -> (known after apply)
      ~ source       = "terraform" -> (known after apply)
      ~ versions     = [ # forces replacement
          ~ {
              ~ version_id = "5a41bfd5-5d1b-4f9e-a282-9235a9ad3bd9" -> (known after apply)
                # (1 unchanged attribute hidden)
            },
        ]
        # (3 unchanged attributes hidden)
    }

  # module.github_bot_worker[0].cloudflare_worker.this will be updated in-place
  ~ resource "cloudflare_worker" "this" {
        id             = "4b5e2696491a41eaaa124f4e2a9855f2"
        name           = "open-inspect-github-bot-primo"
      ~ observability  = {
          ~ logs               = {
              + destinations       = (known after apply)
                # (4 unchanged attributes hidden)
            }
          ~ traces             = {
              + destinations       = (known after apply)
                # (3 unchanged attributes hidden)
            }
            # (2 unchanged attributes hidden)
        }
      ~ references     = {
          ~ dispatch_namespace_outbounds = [] -> (known after apply)
          ~ domains                      = [] -> (known after apply)
          ~ durable_objects              = [] -> (known after apply)
          ~ queues                       = [] -> (known after apply)
          ~ workers                      = [] -> (known after apply)
        } -> (known after apply)
        tags           = []
      ~ updated_on     = "2026-07-26T06:18:37Z" -> (known after apply)
        # (6 unchanged attributes hidden)
    }

  # module.github_bot_worker[0].cloudflare_worker_version.this must be replaced
-/+ resource "cloudflare_worker_version" "this" {
      ~ annotations         = {
          + workers_message      = (known after apply)
          + workers_tag          = (known after apply)
          ~ workers_triggered_by = "create_version_api" -> (known after apply)
        } -> (known after apply)
      ~ bindings            = (sensitive value) # forces replacement
      ~ created_on          = "2026-07-26T06:18:37Z" -> (known after apply)
      ~ id                  = "7aaed149-c286-4edb-9ace-310ce4d3ccd1" -> (known after apply)
      + limits              = (known after apply)
      + main_script_base64  = (known after apply)
      + migration_tag       = (known after apply)
      ~ number              = 41 -> (known after apply)
      ~ source              = "terraform" -> (known after apply)
      ~ startup_time_ms     = 35 -> (known after apply)
      ~ urls                = [
          - "https://7aaed149-open-inspect-github-bot-primo.primo-bf6.workers.dev",
        ] -> (known after apply)
        # (7 unchanged attributes hidden)
    }

  # module.github_bot_worker[0].cloudflare_workers_deployment.this must be replaced
-/+ resource "cloudflare_workers_deployment" "this" {
      ~ annotations  = {
          + workers_message      = (known after apply)
          ~ workers_triggered_by = "deployment" -> (known after apply)
        } -> (known after apply)
      ~ author_email = "alejo@primo.la" -> (known after apply)
      ~ created_on   = "2026-07-26T06:18:38Z" -> (known after apply)
      ~ id           = "2129eb15-5cf8-47a9-9f96-ea9e5ddae4e6" -> (known after apply)
      ~ source       = "terraform" -> (known after apply)
      ~ versions     = [ # forces replacement
          ~ {
              ~ version_id = "7aaed149-c286-4edb-9ace-310ce4d3ccd1" -> (known after apply)
                # (1 unchanged attribute hidden)
            },
        ]
        # (3 unchanged attributes hidden)
    }

  # module.linear_bot_worker[0].cloudflare_worker.this will be updated in-place
  ~ resource "cloudflare_worker" "this" {
        id             = "33782d80e8ff4af9b30b92870084b674"
        name           = "open-inspect-linear-bot-primo"
      ~ observability  = {
          ~ logs               = {
              + destinations       = (known after apply)
                # (4 unchanged attributes hidden)
            }
          ~ traces             = {
              + destinations       = (known after apply)
                # (3 unchanged attributes hidden)
            }
            # (2 unchanged attributes hidden)
        }
      ~ references     = {
          ~ dispatch_namespace_outbounds = [] -> (known after apply)
          ~ domains                      = [] -> (known after apply)
          ~ durable_objects              = [] -> (known after apply)
          ~ queues                       = [] -> (known after apply)
          ~ workers                      = [
              - {
                  - id   = "c208a60c393e45e38eb502346bb7ce1e" -> null
                  - name = "open-inspect-control-plane-primo" -> null
                },
            ] -> (known after apply)
        } -> (known after apply)
        tags           = []
      ~ updated_on     = "2026-07-26T06:18:25Z" -> (known after apply)
        # (6 unchanged attributes hidden)
    }

  # module.linear_bot_worker[0].cloudflare_worker_version.this must be replaced
-/+ resource "cloudflare_worker_version" "this" {
      ~ annotations         = {
          + workers_message      = (known after apply)
          + workers_tag          = (known after apply)
          ~ workers_triggered_by = "create_version_api" -> (known after apply)
        } -> (known after apply)
      ~ bindings            = (sensitive value) # forces replacement
      ~ created_on          = "2026-07-26T06:18:26Z" -> (known after apply)
      ~ id                  = "16b53281-fe2f-4885-a89e-c2239de3b811" -> (known after apply)
      + limits              = (known after apply)
      + main_script_base64  = (known after apply)
      + migration_tag       = (known after apply)
      ~ modules             = [
          - { # forces replacement
              - content_file   = "../../..//packages/linear-bot/dist/index.js" -> null
              - content_sha256 = "2517fbd24658ee26edf46886075207f1ec4681a04695a102b050176899387c27" -> null
              - content_type   = "application/javascript+module" -> null
              - name           = "index.js" -> null
            },
          + { # forces replacement
              + content_file   = "../../..//packages/linear-bot/dist/index.js"
              + content_sha256 = "40fe0740664b29a528bebade6c0cdcb976f112ca86bf7d3f5040e1e82299b9ab"
              + content_type   = "application/javascript+module"
              + name           = "index.js"
            },
        ]
      ~ number              = 54 -> (known after apply)
      ~ source              = "terraform" -> (known after apply)
      ~ startup_time_ms     = 33 -> (known after apply)
      ~ urls                = [
          - "https://16b53281-open-inspect-linear-bot-primo.primo-bf6.workers.dev",
        ] -> (known after apply)
        # (6 unchanged attributes hidden)
    }

  # module.linear_bot_worker[0].cloudflare_workers_deployment.this must be replaced
-/+ resource "cloudflare_workers_deployment" "this" {
      ~ annotations  = {
          + workers_message      = (known after apply)
          ~ workers_triggered_by = "deployment" -> (known after apply)
        } -> (known after apply)
      ~ author_email = "alejo@primo.la" -> (known after apply)
      ~ created_on   = "2026-07-26T06:18:27Z" -> (known after apply)
      ~ id           = "5dc80780-d35d-46b2-8483-290d83a77ea2" -> (known after apply)
      ~ source       = "terraform" -> (known after apply)
      ~ versions     = [ # forces replacement
          ~ {
              ~ version_id = "16b53281-fe2f-4885-a89e-c2239de3b811" -> (known after apply)
                # (1 unchanged attribute hidden)
            },
        ]
        # (3 unchanged attributes hidden)
    }

  # module.slack_bot_worker[0].cloudflare_worker.this will be updated in-place
  ~ resource "cloudflare_worker" "this" {
        id             = "5200e96d69804ea296e1f3a6b39e4243"
        name           = "open-inspect-slack-bot-primo"
      ~ observability  = {
          ~ logs               = {
              + destinations       = (known after apply)
                # (4 unchanged attributes hidden)
            }
          ~ traces             = {
              + destinations       = (known after apply)
                # (3 unchanged attributes hidden)
            }
            # (2 unchanged attributes hidden)
        }
      ~ references     = {
          ~ dispatch_namespace_outbounds = [] -> (known after apply)
          ~ domains                      = [] -> (known after apply)
          ~ durable_objects              = [] -> (known after apply)
          ~ queues                       = [
              - {
                  - queue_consumer_id = "a755a290fd92417fb11c298f9c1d1f40" -> null
                  - queue_id          = "56ef0f3e13bd46a3a39f30c79ec547fa" -> null
                  - queue_name        = "open-inspect-slack-completion-primo" -> null
                },
            ] -> (known after apply)
          ~ workers                      = [
              - {
                  - id   = "c208a60c393e45e38eb502346bb7ce1e" -> null
                  - name = "open-inspect-control-plane-primo" -> null
                },
            ] -> (known after apply)
        } -> (known after apply)
        tags           = []
      ~ updated_on     = "2026-07-26T06:18:25Z" -> (known after apply)
        # (6 unchanged attributes hidden)
    }

  # module.slack_bot_worker[0].cloudflare_worker_version.this must be replaced
-/+ resource "cloudflare_worker_version" "this" {
      ~ annotations         = {
          + workers_message      = (known after apply)
          + workers_tag          = (known after apply)
          ~ workers_triggered_by = "create_version_api" -> (known after apply)
        } -> (known after apply)
      ~ bindings            = (sensitive value) # forces replacement
      ~ created_on          = "2026-07-26T06:18:26Z" -> (known after apply)
      ~ id                  = "6551b217-d1f4-4f1d-8072-253ad7a0d163" -> (known after apply)
      + limits              = (known after apply)
      + main_script_base64  = (known after apply)
      + migration_tag       = (known after apply)
      ~ number              = 57 -> (known after apply)
      ~ source              = "terraform" -> (known after apply)
      ~ startup_time_ms     = 36 -> (known after apply)
      ~ urls                = [
          - "https://6551b217-open-inspect-slack-bot-primo.primo-bf6.workers.dev",
        ] -> (known after apply)
        # (7 unchanged attributes hidden)
    }

  # module.slack_bot_worker[0].cloudflare_workers_deployment.this must be replaced
-/+ resource "cloudflare_workers_deployment" "this" {
      ~ annotations  = {
          + workers_message      = (known after apply)
          ~ workers_triggered_by = "deployment" -> (known after apply)
        } -> (known after apply)
      ~ author_email = "alejo@primo.la" -> (known after apply)
      ~ created_on   = "2026-07-26T06:18:27Z" -> (known after apply)
      ~ id           = "21642fd7-f856-48de-a26d-d501d8817102" -> (known after apply)
      ~ source       = "terraform" -> (known after apply)
      ~ versions     = [ # forces replacement
          ~ {
              ~ version_id = "6551b217-d1f4-4f1d-8072-253ad7a0d163" -> (known after apply)
                # (1 unchanged attribute hidden)
            },
        ]
        # (3 unchanged attributes hidden)
    }

Plan: 17 to add, 4 to change, 16 to destroy.

─────────────────────────────────────────────────────────────────────────────

Saved the plan to: tfplan

To perform exactly these actions, run the following command to apply:
    terraform apply "tfplan"

Pushed by: @NicolasWalter

@NicolasWalter
NicolasWalter merged commit 18a0b55 into main Jul 27, 2026
17 of 18 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d61fb5f68f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +129 to +135
return {
scmUserId: profile.data.subject,
scmLogin: profile.data.login,
displayName: profile.data.displayName ?? profile.data.login,
email: author?.email,
accessTokenEncrypted,
...(token.accessTokenExpiresAt ? { tokenExpiresAt: token.accessTokenExpiresAt.getTime() } : {}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve refresh capability for Better Auth SCM tokens

When GitHub issues an expiring user token, this enrichment copies the access token and expiry into the session but deliberately omits the refresh token without providing another refresh path. After the expiry—such as when a long-running or resumed session creates a PR before another browser prompt refreshes its participant—the existing ParticipantService.refreshTokenLocal() finds no scm_refresh_token_encrypted, and resolveAuthForPR() falls back to the GitHub App token, losing the user's credentials and attribution. Keep Better Auth as the token authority, but make expired session credentials refresh through it (or otherwise preserve a usable refresh capability).

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/control-plane/src/routes/session-create.ts (1)

144-167: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Fail-closed authority resolution is swallowed by best-effort enrichment catches in both route handlers. resolveGitHubCredentialAuthority throws on missing browser-session provenance, an unavailable auth runtime, or corrupt/ambiguous account state. Both call sites evaluate it inside the try whose catch merely warns and continues, converting deliberate integrity failures into a silent degrade to the bot fallback. The genuinely optional case (no linked GitHub account) already returns null without throwing, so it does not depend on this catch.

  • packages/control-plane/src/routes/session-create.ts#L144-L167: hoist the await resolveGitHubCredentialAuthority(ctx, request.headers) call above the try (or rethrow non-enrichment errors) so authority failures produce an error response instead of a warning.
  • packages/control-plane/src/routes/session-prompt.ts#L93-L100: apply the same hoist so prompt enrichment does not mask provenance/integrity failures.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/control-plane/src/routes/session-create.ts` around lines 144 - 167,
Move the resolveGitHubCredentialAuthority call outside the best-effort
enrichment try/catch in session-create.ts (lines 144-167), and apply the same
change in session-prompt.ts (lines 93-100). Pass the resolved authority into
resolveGitHubEnrichmentForRequest so authority/provenance failures propagate as
error responses, while the catch continues handling only optional enrichment
failures.
🧹 Nitpick comments (14)
packages/linear-bot/src/utils/integration-config.test.ts (1)

81-89: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse the named fallback configuration.

Export/import and assert against DEFAULT_CONFIG rather than repeating its literal values in both fallback tests. As per coding guidelines, “Define each default value exactly once in a named constant and import or reuse that constant everywhere.”

Also applies to: 103-111

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/linear-bot/src/utils/integration-config.test.ts` around lines 81 -
89, Update the fallback tests around the configuration resolution assertions to
import and compare against the named DEFAULT_CONFIG constant instead of
duplicating its literal values. Apply this to both fallback test cases,
preserving the existing resolve behavior while ensuring defaults are defined and
validated in one place.

Source: Coding guidelines

packages/control-plane/wrangler.jsonc (1)

5-6: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Compatibility date and flags are declared in two places and must match Terraform. The test worker runtime is now configured independently in both files, so they can drift from each other and from the Terraform-managed production worker.

  • packages/control-plane/wrangler.jsonc#L5-L6: confirm 2024-09-23 + nodejs_compat matches workers-control-plane.tf; the date moves backward from 2024-12-30.
  • packages/control-plane/vitest.integration.config.ts#L48-L51: derive compatibilityDate/compatibilityFlags from the loaded wrangler.jsonc instead of re-hardcoding them.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/control-plane/wrangler.jsonc` around lines 5 - 6, Align
packages/control-plane/wrangler.jsonc lines 5-6 with workers-control-plane.tf
using compatibility date 2024-09-23 and the nodejs_compat flag. Update
packages/control-plane/vitest.integration.config.ts lines 48-51 to load
compatibilityDate and compatibilityFlags from the parsed wrangler.jsonc
configuration instead of hard-coding them, keeping both test and production
runtime settings synchronized.

Source: Coding guidelines

packages/control-plane/test/integration/helpers.ts (1)

37-44: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Seeding on every web request adds four D1 writes per call.

testBrowserSessionCookie() re-runs the batch for each serviceFetch. Consider memoizing the cookie per test file (reset alongside cleanD1Tables()) to cut integration-test I/O.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/control-plane/test/integration/helpers.ts` around lines 37 - 44, The
testBrowserSessionCookie function currently performs its D1 seeding batch on
every serviceFetch. Memoize the generated cookie for the test file so repeated
calls reuse it without rerunning env.DB.batch, and clear that cached value
alongside cleanD1Tables() so each test starts with fresh state.
packages/control-plane/vitest.integration.config.ts (1)

85-105: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Stack-path match on /better-auth/dist/api/routes/ is brittle across upgrades.

If the package layout changes on a better-auth bump, redirect rejections stop matching and integration suites fail as unhandled errors. Consider matching on name === "APIError" + 3xx status alone, or asserting only better-auth in the stack.

♻️ Looser match
-        betterAuthStack?.includes("/better-auth/dist/api/routes/")
+        betterAuthStack?.includes("better-auth")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/control-plane/vitest.integration.config.ts` around lines 85 - 105,
Loosen the filtering condition in onUnhandledError so Better Auth redirect
control flow remains recognized across package layout changes. Keep the APIError
name and 3xx status checks, but replace the brittle
"/better-auth/dist/api/routes/" stack-path requirement with a stable better-auth
stack check, or rely on those APIError/3xx checks alone; continue returning
false only for the intended redirects.
packages/web/src/app/api/sessions/[id]/attachments/[attachmentId]/route.ts (1)

45-50: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Extract the shared passthrough-header allowlist. Both binary proxies now hardcode the same ["Content-Type", "Content-Range", "Accept-Ranges", "ETag"] list and copy loop, so any future header-policy change (like this Content-Length removal) has to be made twice.

  • packages/web/src/app/api/sessions/[id]/attachments/[attachmentId]/route.ts#L45-L50: import a shared constant/helper instead of the inline array and loop.
  • packages/web/src/app/api/sessions/[id]/media/[artifactId]/route.ts#L42-L47: reuse the same shared constant/helper.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/app/api/sessions/`[id]/attachments/[attachmentId]/route.ts
around lines 45 - 50, Extract the shared passthrough-header allowlist and
copying logic from the binary proxy routes into a reusable constant or helper.
Update
packages/web/src/app/api/sessions/[id]/attachments/[attachmentId]/route.ts lines
45-50 and packages/web/src/app/api/sessions/[id]/media/[artifactId]/route.ts
lines 42-47 to reuse it, preserving the current Content-Type, Content-Range,
Accept-Ranges, and ETag policy.
terraform/d1/migrations/0048_better_auth_core.sql (1)

22-35: 🚀 Performance & Scalability | 🔵 Trivial

Consider an index on auth_sessions(expiresAt) for expiry sweeps.

Better Auth's session cleanup / expired-session deletion scans by expiresAt; without an index that is a full table scan as sessions accumulate.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@terraform/d1/migrations/0048_better_auth_core.sql` around lines 22 - 35,
Update the auth_sessions schema migration to add an index on the expiresAt
column for expiry cleanup queries. Define the index alongside
auth_sessions_userId_idx, preserving the existing table and userId index
definitions.
packages/control-plane/src/auth/service/request-authenticator.ts (1)

39-71: 🔒 Security & Privacy | 🔵 Trivial | 🏗️ Heavy lift

Nonce reuse is detected but not rejected.

recordNonce logs and returns on a hit, so a captured signature can be replayed within TOKEN_VALIDITY_MS. The comment says "log-only for now" — worth tracking as a follow-up to enforce rejection (in-isolate state makes this best-effort; a durable store would be needed for a real guarantee).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/control-plane/src/auth/service/request-authenticator.ts` around
lines 39 - 71, Track nonce reuse enforcement as a follow-up from recordNonce:
preserve the current log-only behavior for now, but add a clear TODO or issue
reference documenting that reused nonces must eventually be rejected within
TOKEN_VALIDITY_MS, acknowledging the best-effort in-isolate limitation.
packages/control-plane/src/auth/authenticate.test.ts (1)

312-374: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider covering the two 500 paths.

authenticate also returns 500 when ctx.getUserAuth is absent and when authenticateSession throws (SessionIntegrityError vs runtime). Neither branch is exercised here, and both are security-relevant fail-closed paths.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/control-plane/src/auth/authenticate.test.ts` around lines 312 - 374,
Extend the compound browser credentials tests around createUserAuthContext and
authenticate to cover both 500 fail-closed paths: a context without
ctx.getUserAuth, and an authenticateSession failure that distinguishes
SessionIntegrityError from an unexpected runtime error. Assert each branch’s
status and error response, while preserving the existing successful and
absent-session coverage.
packages/control-plane/src/source-control/github-credential-authority.test.ts (1)

56-135: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add cases for the remaining throw paths.

Uncovered branches in resolveGitHubCredentialAuthority: schema-failure on a malformed listUserAccounts payload (same "corrupt" message but a different cause), missing getUserAuth, and a non-user principal carrying authentication.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/control-plane/src/source-control/github-credential-authority.test.ts`
around lines 56 - 135, Add tests for the remaining throw branches in
resolveGitHubCredentialAuthority: make listUserAccounts return a malformed
payload and assert the “GitHub account authority is corrupt” error, omit
getUserAuth and assert its expected failure, and provide a non-user principal
with authentication to verify that path rejects appropriately. Reuse the
existing createContext/createUserContext helpers and preserve the current
assertions.
packages/control-plane/src/auth/user/session-authenticator.test.ts (1)

1-49: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add a test for the malformed-session branch.

Coverage exists for null and cross-user sessions, but not for sessionSchema.safeParse failure (e.g., missing session.id), which throws SessionIntegrityError("Better Auth returned a malformed session") in session-authenticator.ts. Consider adding a case asserting this throw for a candidate missing required fields.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/control-plane/src/auth/user/session-authenticator.test.ts` around
lines 1 - 49, Add a test in the authenticateSession suite for a Better Auth
result whose session omits a required field such as id, and assert that
authenticateSession rejects with “Better Auth returned a malformed session”
(SessionIntegrityError). Keep the existing null and cross-user cases unchanged.
packages/control-plane/src/auth/user/providers/github-identity.ts (1)

6-11: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

GitHub API base URL/version/user-agent are duplicated instead of centralized. github-identity.ts defines local, unexported GITHUB_API_URL/GITHUB_API_VERSION constants, and admission-policy.ts independently hardcodes the same base URL and API version string plus its own literal user-agent — one root cause: no shared constants module for these GitHub API literals (unlike DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS, which is already correctly centralized in providers/constants.ts).

  • packages/control-plane/src/auth/user/providers/github-identity.ts#L6-L11: export GITHUB_API_URL, GITHUB_API_VERSION (and a shared default user-agent) from providers/constants.ts instead of keeping them file-local.
  • packages/control-plane/src/auth/user/admission-policy.ts#L130-L138: import and reuse the shared constants instead of re-hardcoding "https://api.github.com/user/memberships/orgs/", "2022-11-28", and "Open-Inspect-Control-Plane".

As per coding guidelines, "Define each default value exactly once in a named constant and import or reuse that constant everywhere."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/control-plane/src/auth/user/providers/github-identity.ts` around
lines 6 - 11, The GitHub API URL, API version, and default user-agent are
duplicated instead of centralized. In
packages/control-plane/src/auth/user/providers/github-identity.ts lines 6-11,
move or export these named constants through providers/constants.ts; in
packages/control-plane/src/auth/user/admission-policy.ts lines 130-138, import
and reuse them for the memberships URL, API version, and user-agent instead of
literals.

Source: Coding guidelines

packages/control-plane/src/routes/browser-auth.test.ts (1)

4-27: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider covering the non-session branch and the service:web gate.

Only the get-session short-circuit is exercised. The auth.handler(request) fallback and requireWebService rejecting non-service:web principals are the security-relevant paths and currently untested here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/control-plane/src/routes/browser-auth.test.ts` around lines 4 - 27,
Add tests alongside the existing forwardBrowserAuthRequest test to cover the
non-session request path invoking auth.handler(request), and the
requireWebService gate rejecting principals without the service:web role. Reuse
the visible auth/request setup patterns and assert the fallback response plus
the rejection behavior.
packages/control-plane/src/types.ts (1)

100-104: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document the admission-policy variables, especially UNSAFE_ALLOW_ALL_USERS.

Every neighbouring entry carries a trailing comment describing its format and effect. These five drive who may sign in, and UNSAFE_ALLOW_ALL_USERS disables the allowlist entirely — note the expected format (comma-separated?) and the security implication inline.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/control-plane/src/types.ts` around lines 100 - 104, The
admission-policy entries in the environment-variable type definition lack inline
documentation. Add trailing comments to ALLOWED_USERS, ALLOWED_EMAIL_DOMAINS,
ALLOWED_EMAILS, ALLOWED_GITHUB_ORGS, and UNSAFE_ALLOW_ALL_USERS describing each
expected format and access effect; explicitly document that
UNSAFE_ALLOW_ALL_USERS disables the allowlist and its security implication.
packages/control-plane/src/routes/browser-auth.ts (1)

36-42: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the session path into a shared constant. /api/auth/get-session is duplicated here and in BROWSER_AUTH_PROXY_ROUTES; centralizing it avoids drift if the auth path changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/control-plane/src/routes/browser-auth.ts` around lines 36 - 42,
Define a shared constant for the "/api/auth/get-session" path and use it in the
GET condition within the browser-auth request handler and in
BROWSER_AUTH_PROXY_ROUTES. Remove the duplicated string literals while
preserving the existing routing behavior.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/control-plane/src/auth/user/better-auth.ts`:
- Around line 66-76: Update the GitHub provider configuration in the
socialProviders setup to retain disableDefaultScope: true while explicitly
supplying scopes for read:user and user:email. Preserve any values from
config.github and ensure the explicit scope array is applied so GitHub sign-in
can resolve the user profile and email.
- Around line 45-50: Update the Better Auth rateLimit configuration to use
database-backed storage instead of memory, and add the corresponding Better Auth
rate-limit migration/table required by the database adapter. Preserve the
existing enabled, window, and max settings.

In `@packages/control-plane/src/db/browser-auth-legacy-migration.test.ts`:
- Around line 1-19: Update the repository’s Node.js version constraint to
require >=22.5.0, locating the package/toolchain configuration that defines the
supported Node 22 range. Keep the existing version policy unchanged apart from
raising the minimum needed by the node:sqlite import in this test.

In `@packages/control-plane/src/session/identity.ts`:
- Around line 66-79: Update browser GitHub account matching to compare
account.subject with browserGitHubAccountInfoSchema’s data.subject, not user.id;
retain user.id exclusively as the Better Auth user record identifier.

In `@packages/control-plane/src/worker-build.test.ts`:
- Around line 14-21: Add timeout: WORKER_BUILD_TIMEOUT_MS to both execFileSync
calls in the worker build test, covering the shared-package and worker-package
npm builds while preserving their existing cwd and stdio options.

In `@packages/control-plane/test/integration/browser-auth.test.ts`:
- Around line 1-15: Register shared-D1 cleanup for both integration suites: in
packages/control-plane/test/integration/browser-auth.test.ts lines 1-15 and
packages/control-plane/test/integration/browser-auth-router.test.ts lines 1-10,
import cleanD1Tables from ./cleanup and add beforeEach(cleanD1Tables) before the
describe blocks.

In `@packages/control-plane/test/integration/helpers.ts`:
- Around line 15-28: Update signCookieValue to encode the HMAC signature as
unpadded base64url instead of standard base64 from btoa. Convert the signature
bytes to base64, replace + and / with - and _, remove trailing padding, and
retain the existing cookie value encoding and signing flow.

In `@packages/web/src/lib/browser-auth-proxy.ts`:
- Around line 55-60: Update trustedClientIp to obtain Cloudflare metadata
through getCloudflareContext() instead of checking for a cf property on the
Request. Preserve the Vercel X-Vercel-Forwarded-For behavior, and read
CF-Connecting-IP from the Cloudflare context for non-Vercel deployments.

In `@packages/web/src/lib/control-plane-transport.ts`:
- Around line 90-106: The unconditional timeout in the transport path also
aborts streamed response bodies after headers are received. Update the
fetch-options flow around boundedFetchOptions and the control-plane callers that
proxy attachment/media/diff streams so streaming requests can explicitly skip
the internal CONTROL_PLANE_FETCH_TIMEOUT_MS signal, while retaining the timeout
by default for normal requests and preserving caller-provided signal handling.

In `@packages/web/src/lib/server-auth-session.ts`:
- Around line 28-47: Update getServerAuthSession to contain failures from
serializeBrowserSessionCookies, the non-success dispatchBrowserAuthRequest
response, and browserAuthSessionResponseSchema.parse so callers receive the
intended unauthenticated result or a consistently handled authentication error
instead of an unhandled exception. Prefer returning null for malformed cookies
and invalid session payloads, and ensure any deliberately thrown error is
handled consistently by all consumers.

In `@terraform/d1/migrations/0049_backfill_better_auth_accounts.sql`:
- Around line 21-46: Add an ON CONFLICT(id) DO NOTHING clause to the INSERT into
auth_users in the backfill migration, preserving the existing SELECT and NOT
EXISTS filtering while preventing duplicate primary-key rows from aborting the
migration.

In `@terraform/environments/production/.terraform.lock.hcl`:
- Around line 88-103: Update the hashicorp/random lock entry’s hashes list to
include the complete platform coverage for linux_amd64 and darwin_arm64,
regenerating it with terraform providers lock and preserving the existing
checksums alongside the additional h1 hash.

---

Outside diff comments:
In `@packages/control-plane/src/routes/session-create.ts`:
- Around line 144-167: Move the resolveGitHubCredentialAuthority call outside
the best-effort enrichment try/catch in session-create.ts (lines 144-167), and
apply the same change in session-prompt.ts (lines 93-100). Pass the resolved
authority into resolveGitHubEnrichmentForRequest so authority/provenance
failures propagate as error responses, while the catch continues handling only
optional enrichment failures.

---

Nitpick comments:
In `@packages/control-plane/src/auth/authenticate.test.ts`:
- Around line 312-374: Extend the compound browser credentials tests around
createUserAuthContext and authenticate to cover both 500 fail-closed paths: a
context without ctx.getUserAuth, and an authenticateSession failure that
distinguishes SessionIntegrityError from an unexpected runtime error. Assert
each branch’s status and error response, while preserving the existing
successful and absent-session coverage.

In `@packages/control-plane/src/auth/service/request-authenticator.ts`:
- Around line 39-71: Track nonce reuse enforcement as a follow-up from
recordNonce: preserve the current log-only behavior for now, but add a clear
TODO or issue reference documenting that reused nonces must eventually be
rejected within TOKEN_VALIDITY_MS, acknowledging the best-effort in-isolate
limitation.

In `@packages/control-plane/src/auth/user/providers/github-identity.ts`:
- Around line 6-11: The GitHub API URL, API version, and default user-agent are
duplicated instead of centralized. In
packages/control-plane/src/auth/user/providers/github-identity.ts lines 6-11,
move or export these named constants through providers/constants.ts; in
packages/control-plane/src/auth/user/admission-policy.ts lines 130-138, import
and reuse them for the memberships URL, API version, and user-agent instead of
literals.

In `@packages/control-plane/src/auth/user/session-authenticator.test.ts`:
- Around line 1-49: Add a test in the authenticateSession suite for a Better
Auth result whose session omits a required field such as id, and assert that
authenticateSession rejects with “Better Auth returned a malformed session”
(SessionIntegrityError). Keep the existing null and cross-user cases unchanged.

In `@packages/control-plane/src/routes/browser-auth.test.ts`:
- Around line 4-27: Add tests alongside the existing forwardBrowserAuthRequest
test to cover the non-session request path invoking auth.handler(request), and
the requireWebService gate rejecting principals without the service:web role.
Reuse the visible auth/request setup patterns and assert the fallback response
plus the rejection behavior.

In `@packages/control-plane/src/routes/browser-auth.ts`:
- Around line 36-42: Define a shared constant for the "/api/auth/get-session"
path and use it in the GET condition within the browser-auth request handler and
in BROWSER_AUTH_PROXY_ROUTES. Remove the duplicated string literals while
preserving the existing routing behavior.

In
`@packages/control-plane/src/source-control/github-credential-authority.test.ts`:
- Around line 56-135: Add tests for the remaining throw branches in
resolveGitHubCredentialAuthority: make listUserAccounts return a malformed
payload and assert the “GitHub account authority is corrupt” error, omit
getUserAuth and assert its expected failure, and provide a non-user principal
with authentication to verify that path rejects appropriately. Reuse the
existing createContext/createUserContext helpers and preserve the current
assertions.

In `@packages/control-plane/src/types.ts`:
- Around line 100-104: The admission-policy entries in the environment-variable
type definition lack inline documentation. Add trailing comments to
ALLOWED_USERS, ALLOWED_EMAIL_DOMAINS, ALLOWED_EMAILS, ALLOWED_GITHUB_ORGS, and
UNSAFE_ALLOW_ALL_USERS describing each expected format and access effect;
explicitly document that UNSAFE_ALLOW_ALL_USERS disables the allowlist and its
security implication.

In `@packages/control-plane/test/integration/helpers.ts`:
- Around line 37-44: The testBrowserSessionCookie function currently performs
its D1 seeding batch on every serviceFetch. Memoize the generated cookie for the
test file so repeated calls reuse it without rerunning env.DB.batch, and clear
that cached value alongside cleanD1Tables() so each test starts with fresh
state.

In `@packages/control-plane/vitest.integration.config.ts`:
- Around line 85-105: Loosen the filtering condition in onUnhandledError so
Better Auth redirect control flow remains recognized across package layout
changes. Keep the APIError name and 3xx status checks, but replace the brittle
"/better-auth/dist/api/routes/" stack-path requirement with a stable better-auth
stack check, or rely on those APIError/3xx checks alone; continue returning
false only for the intended redirects.

In `@packages/control-plane/wrangler.jsonc`:
- Around line 5-6: Align packages/control-plane/wrangler.jsonc lines 5-6 with
workers-control-plane.tf using compatibility date 2024-09-23 and the
nodejs_compat flag. Update packages/control-plane/vitest.integration.config.ts
lines 48-51 to load compatibilityDate and compatibilityFlags from the parsed
wrangler.jsonc configuration instead of hard-coding them, keeping both test and
production runtime settings synchronized.

In `@packages/linear-bot/src/utils/integration-config.test.ts`:
- Around line 81-89: Update the fallback tests around the configuration
resolution assertions to import and compare against the named DEFAULT_CONFIG
constant instead of duplicating its literal values. Apply this to both fallback
test cases, preserving the existing resolve behavior while ensuring defaults are
defined and validated in one place.

In `@packages/web/src/app/api/sessions/`[id]/attachments/[attachmentId]/route.ts:
- Around line 45-50: Extract the shared passthrough-header allowlist and copying
logic from the binary proxy routes into a reusable constant or helper. Update
packages/web/src/app/api/sessions/[id]/attachments/[attachmentId]/route.ts lines
45-50 and packages/web/src/app/api/sessions/[id]/media/[artifactId]/route.ts
lines 42-47 to reuse it, preserving the current Content-Type, Content-Range,
Accept-Ranges, and ETag policy.

In `@terraform/d1/migrations/0048_better_auth_core.sql`:
- Around line 22-35: Update the auth_sessions schema migration to add an index
on the expiresAt column for expiry cleanup queries. Define the index alongside
auth_sessions_userId_idx, preserving the existing table and userId index
definitions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c2cbb6c1-914f-41e2-9120-b14ee6ea7adc

📥 Commits

Reviewing files that changed from the base of the PR and between ff9e91b and d61fb5f.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (191)
  • .github/workflows/ci.yml
  • .github/workflows/deploy-web.yml
  • docs/GETTING_STARTED.md
  • docs/SETUP_GUIDE.md
  • eslint.config.js
  • packages/control-plane/README.md
  • packages/control-plane/package.json
  • packages/control-plane/src/auth/auth-encryption.test.ts
  • packages/control-plane/src/auth/auth-encryption.ts
  • packages/control-plane/src/auth/authenticate.test.ts
  • packages/control-plane/src/auth/authenticate.ts
  • packages/control-plane/src/auth/identity-enforcement.test.ts
  • packages/control-plane/src/auth/identity-enforcement.ts
  • packages/control-plane/src/auth/index.ts
  • packages/control-plane/src/auth/oauth-authorization-service.test.ts
  • packages/control-plane/src/auth/oauth-authorization-service.ts
  • packages/control-plane/src/auth/oauth-flow-state.ts
  • packages/control-plane/src/auth/oauth-flow-verifier.ts
  • packages/control-plane/src/auth/pkce.test.ts
  • packages/control-plane/src/auth/pkce.ts
  • packages/control-plane/src/auth/principal.ts
  • packages/control-plane/src/auth/provider-credential-cipher.ts
  • packages/control-plane/src/auth/providers/github.test.ts
  • packages/control-plane/src/auth/providers/github.ts
  • packages/control-plane/src/auth/providers/google.test.ts
  • packages/control-plane/src/auth/providers/google.ts
  • packages/control-plane/src/auth/providers/oidc-authorization-code-client.ts
  • packages/control-plane/src/auth/providers/types.test.ts
  • packages/control-plane/src/auth/providers/types.ts
  • packages/control-plane/src/auth/result.ts
  • packages/control-plane/src/auth/service/callback-signing.ts
  • packages/control-plane/src/auth/service/config.ts
  • packages/control-plane/src/auth/service/request-authenticator.ts
  • packages/control-plane/src/auth/subject-verification.test.ts
  • packages/control-plane/src/auth/subject-verification.ts
  • packages/control-plane/src/auth/token-exchange.ts
  • packages/control-plane/src/auth/user/admission-policy.test.ts
  • packages/control-plane/src/auth/user/admission-policy.ts
  • packages/control-plane/src/auth/user/better-auth.ts
  • packages/control-plane/src/auth/user/canonical-user-projection.ts
  • packages/control-plane/src/auth/user/provider-credential.ts
  • packages/control-plane/src/auth/user/provider-profile.ts
  • packages/control-plane/src/auth/user/providers/constants.ts
  • packages/control-plane/src/auth/user/providers/github-identity.test.ts
  • packages/control-plane/src/auth/user/providers/github-identity.ts
  • packages/control-plane/src/auth/user/providers/github-profile.test.ts
  • packages/control-plane/src/auth/user/providers/github-profile.ts
  • packages/control-plane/src/auth/user/providers/google-profile.test.ts
  • packages/control-plane/src/auth/user/providers/google-profile.ts
  • packages/control-plane/src/auth/user/providers/types.ts
  • packages/control-plane/src/auth/user/runtime.test.ts
  • packages/control-plane/src/auth/user/runtime.ts
  • packages/control-plane/src/auth/user/session-authenticator.test.ts
  • packages/control-plane/src/auth/user/session-authenticator.ts
  • packages/control-plane/src/auth/user/sign-in-provider.test.ts
  • packages/control-plane/src/auth/user/sign-in-provider.ts
  • packages/control-plane/src/auth/web-session-tokens.test.ts
  • packages/control-plane/src/auth/web-session-tokens.ts
  • packages/control-plane/src/db/api-tokens.test.ts
  • packages/control-plane/src/db/api-tokens.ts
  • packages/control-plane/src/db/browser-auth-legacy-migration.test.ts
  • packages/control-plane/src/db/browser-auth-sessions.ts
  • packages/control-plane/src/db/canonical-user-projection.ts
  • packages/control-plane/src/db/errors.ts
  • packages/control-plane/src/db/oauth-authorization-codes.ts
  • packages/control-plane/src/db/oauth-flow-state.ts
  • packages/control-plane/src/db/provider-credentials.ts
  • packages/control-plane/src/router.analytics.test.ts
  • packages/control-plane/src/router.auth.test.ts
  • packages/control-plane/src/router.create-session.test.ts
  • packages/control-plane/src/router.provider-identities.test.ts
  • packages/control-plane/src/router.scm-credentials.test.ts
  • packages/control-plane/src/router.session-prompt.test.ts
  • packages/control-plane/src/router.spawn-child.test.ts
  • packages/control-plane/src/router.ts
  • packages/control-plane/src/routes/auth-tokens.ts
  • packages/control-plane/src/routes/automations.test.ts
  • packages/control-plane/src/routes/browser-auth.test.ts
  • packages/control-plane/src/routes/browser-auth.ts
  • packages/control-plane/src/routes/provider-identities.test.ts
  • packages/control-plane/src/routes/provider-identities.ts
  • packages/control-plane/src/routes/session-create.ts
  • packages/control-plane/src/routes/session-prompt.ts
  • packages/control-plane/src/routes/session-runtime-proxy.test.ts
  • packages/control-plane/src/routes/shared.ts
  • packages/control-plane/src/scheduler/durable-object.ts
  • packages/control-plane/src/session/callback-notification-service.ts
  • packages/control-plane/src/session/identity.test.ts
  • packages/control-plane/src/session/identity.ts
  • packages/control-plane/src/source-control/github-credential-authority.test.ts
  • packages/control-plane/src/source-control/github-credential-authority.ts
  • packages/control-plane/src/types.ts
  • packages/control-plane/src/types/error.d.ts
  • packages/control-plane/src/worker-build.test.ts
  • packages/control-plane/test/integration/auth-tokens.test.ts
  • packages/control-plane/test/integration/auth.test.ts
  • packages/control-plane/test/integration/browser-auth-callback.test.ts
  • packages/control-plane/test/integration/browser-auth-router.test.ts
  • packages/control-plane/test/integration/browser-auth-sessions.test.ts
  • packages/control-plane/test/integration/browser-auth.test.ts
  • packages/control-plane/test/integration/cleanup.ts
  • packages/control-plane/test/integration/helpers.ts
  • packages/control-plane/test/integration/oauth-authorization-codes.test.ts
  • packages/control-plane/test/integration/oauth-flow-state.test.ts
  • packages/control-plane/test/integration/provider-credentials.test.ts
  • packages/control-plane/test/integration/service-auth.test.ts
  • packages/control-plane/tsconfig.test.json
  • packages/control-plane/vitest.integration.config.ts
  • packages/control-plane/wrangler.jsonc
  • packages/linear-bot/src/classifier/index.test.ts
  • packages/linear-bot/src/classifier/index.ts
  • packages/linear-bot/src/utils/integration-config.test.ts
  • packages/linear-bot/src/utils/integration-config.ts
  • packages/linear-bot/src/webhook-handler.test.ts
  • packages/linear-bot/src/webhook-handler.ts
  • packages/shared/src/browser-auth-routes.test.ts
  • packages/shared/src/browser-auth-routes.ts
  • packages/shared/src/index.ts
  • packages/web/.env.example
  • packages/web/README.md
  • packages/web/package.json
  • packages/web/src/app/access-denied/page.tsx
  • packages/web/src/app/api/auth/[...auth]/route.test.ts
  • packages/web/src/app/api/auth/[...auth]/route.ts
  • packages/web/src/app/api/auth/[...nextauth]/route.ts
  • packages/web/src/app/api/auth/oi-refresh/route.test.ts
  • packages/web/src/app/api/auth/oi-refresh/route.ts
  • packages/web/src/app/api/automations/route.test.ts
  • packages/web/src/app/api/automations/route.ts
  • packages/web/src/app/api/sessions/[id]/attachments/[attachmentId]/route.test.ts
  • packages/web/src/app/api/sessions/[id]/attachments/[attachmentId]/route.ts
  • packages/web/src/app/api/sessions/[id]/diff/[revisionId]/files/[fileId]/route.test.ts
  • packages/web/src/app/api/sessions/[id]/diff/[revisionId]/files/[fileId]/route.ts
  • packages/web/src/app/api/sessions/[id]/diff/retry/route.ts
  • packages/web/src/app/api/sessions/[id]/media/[artifactId]/route.test.ts
  • packages/web/src/app/api/sessions/[id]/media/[artifactId]/route.ts
  • packages/web/src/app/api/sessions/[id]/ws-token/route.test.ts
  • packages/web/src/app/api/sessions/[id]/ws-token/route.ts
  • packages/web/src/app/api/sessions/route.test.ts
  • packages/web/src/app/api/sessions/route.ts
  • packages/web/src/app/providers.test.tsx
  • packages/web/src/app/providers.tsx
  • packages/web/src/components/sidebar-layout.test.tsx
  • packages/web/src/components/web-session-gate.integration.test.tsx
  • packages/web/src/components/web-session-gate.test.tsx
  • packages/web/src/components/web-session-gate.tsx
  • packages/web/src/lib/access-control.test.ts
  • packages/web/src/lib/access-control.ts
  • packages/web/src/lib/auth-session.test.tsx
  • packages/web/src/lib/auth-session.tsx
  • packages/web/src/lib/auth.test.ts
  • packages/web/src/lib/auth.ts
  • packages/web/src/lib/browser-auth-proxy.test.ts
  • packages/web/src/lib/browser-auth-proxy.ts
  • packages/web/src/lib/browser-auth-session-contract.ts
  • packages/web/src/lib/browser-session-cookie.test.ts
  • packages/web/src/lib/browser-session-cookie.ts
  • packages/web/src/lib/build-auth-identity.test.ts
  • packages/web/src/lib/build-auth-identity.ts
  • packages/web/src/lib/client-auth-boundary-eslint.test.ts
  • packages/web/src/lib/control-plane-transport.test.ts
  • packages/web/src/lib/control-plane-transport.ts
  • packages/web/src/lib/control-plane.test.ts
  • packages/web/src/lib/control-plane.ts
  • packages/web/src/lib/current-user.test.ts
  • packages/web/src/lib/current-user.ts
  • packages/web/src/lib/github-email-schema.test.ts
  • packages/web/src/lib/github-email-schema.ts
  • packages/web/src/lib/github-org-membership.test.ts
  • packages/web/src/lib/github-org-membership.ts
  • packages/web/src/lib/oi-session.test.ts
  • packages/web/src/lib/oi-session.ts
  • packages/web/src/lib/server-auth-boundary-eslint.test.ts
  • packages/web/src/lib/server-auth-session.test.ts
  • packages/web/src/lib/server-auth-session.ts
  • packages/web/src/lib/session-cookie.test.ts
  • packages/web/src/lib/session-cookie.ts
  • packages/web/src/lib/site-config.ts
  • scripts/wrangler-secrets.sh
  • terraform/README.md
  • terraform/d1/migrations/0048_better_auth_core.sql
  • terraform/d1/migrations/0049_backfill_better_auth_accounts.sql
  • terraform/d1/migrations/0050_purge_retired_api_tokens.sql
  • terraform/environments/production/.terraform.lock.hcl
  • terraform/environments/production/checks.tf
  • terraform/environments/production/locals.tf
  • terraform/environments/production/terraform.tfvars.example
  • terraform/environments/production/variables.tf
  • terraform/environments/production/web-cloudflare.tf
  • terraform/environments/production/web-vercel.tf
  • terraform/environments/production/workers-control-plane.tf
💤 Files with no reviewable changes (50)
  • packages/web/src/lib/auth.ts
  • packages/web/src/lib/control-plane-transport.test.ts
  • packages/web/src/app/api/auth/[...nextauth]/route.ts
  • packages/control-plane/src/auth/providers/google.test.ts
  • packages/control-plane/test/integration/oauth-flow-state.test.ts
  • packages/control-plane/src/auth/index.ts
  • packages/web/src/app/api/auth/oi-refresh/route.ts
  • packages/web/src/app/api/auth/oi-refresh/route.test.ts
  • packages/control-plane/src/auth/subject-verification.test.ts
  • packages/control-plane/src/auth/providers/google.ts
  • packages/web/src/components/web-session-gate.tsx
  • packages/control-plane/src/auth/pkce.test.ts
  • packages/control-plane/src/db/api-tokens.test.ts
  • packages/control-plane/src/routes/provider-identities.test.ts
  • packages/control-plane/src/auth/oauth-flow-verifier.ts
  • packages/control-plane/src/auth/providers/github.ts
  • packages/control-plane/src/auth/provider-credential-cipher.ts
  • packages/control-plane/src/auth/oauth-authorization-service.ts
  • packages/control-plane/src/router.provider-identities.test.ts
  • packages/control-plane/src/auth/web-session-tokens.test.ts
  • packages/control-plane/src/auth/auth-encryption.test.ts
  • packages/control-plane/test/integration/oauth-authorization-codes.test.ts
  • packages/control-plane/test/integration/browser-auth-sessions.test.ts
  • packages/web/src/lib/auth.test.ts
  • packages/control-plane/src/routes/provider-identities.ts
  • packages/control-plane/src/db/oauth-flow-state.ts
  • packages/control-plane/src/auth/providers/types.test.ts
  • packages/web/src/lib/access-control.ts
  • packages/control-plane/src/auth/pkce.ts
  • packages/control-plane/src/auth/providers/github.test.ts
  • packages/control-plane/test/integration/provider-credentials.test.ts
  • packages/web/src/components/web-session-gate.integration.test.tsx
  • packages/control-plane/src/db/api-tokens.ts
  • .github/workflows/deploy-web.yml
  • packages/control-plane/src/auth/oauth-authorization-service.test.ts
  • packages/web/src/components/web-session-gate.test.tsx
  • packages/control-plane/src/auth/auth-encryption.ts
  • packages/web/src/lib/access-control.test.ts
  • packages/control-plane/src/auth/providers/oidc-authorization-code-client.ts
  • packages/web/package.json
  • packages/control-plane/src/auth/oauth-flow-state.ts
  • packages/control-plane/src/auth/providers/types.ts
  • packages/control-plane/src/auth/subject-verification.ts
  • packages/control-plane/src/routes/auth-tokens.ts
  • packages/control-plane/test/integration/auth-tokens.test.ts
  • packages/control-plane/src/auth/web-session-tokens.ts
  • packages/control-plane/src/auth/token-exchange.ts
  • packages/control-plane/src/db/browser-auth-sessions.ts
  • packages/control-plane/src/db/oauth-authorization-codes.ts
  • packages/control-plane/src/db/provider-credentials.ts

Comment on lines +45 to +50
rateLimit: {
enabled: true,
window: 60,
max: 100,
storage: "memory",
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== File context ==\n'
sed -n '1,220p' packages/control-plane/src/auth/user/better-auth.ts

printf '\n== Search for database wiring and rate limit config ==\n'
rg -n 'rateLimit|storage: "memory"|storage: "database"|config\.database|database:' packages/control-plane/src/auth -S

printf '\n== Better Auth references in repo ==\n'
rg -n 'better-auth|Better Auth|rate limit|rateLimit' packages/control-plane -S

printf '\n== Relevant file inventory ==\n'
git ls-files 'packages/control-plane/src/auth/**'

Repository: primo-devs/primo-bg-coding-agent

Length of output: 13648


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Browser auth integration tests ==\n'
sed -n '130,230p' packages/control-plane/test/integration/browser-auth.test.ts

printf '\n== Better Auth migration/schema references ==\n'
rg -n 'rate limit|rateLimit|auth_verifications|auth_sessions|auth_accounts|auth_users|migrate|migration' packages/control-plane/test/integration packages/control-plane/src/db packages/control-plane/src/auth -S

printf '\n== D1 schema/migration inventory ==\n'
git ls-files 'packages/control-plane/**/migration*' 'packages/control-plane/**/migrations/**' 'packages/control-plane/**/db/**'

Repository: primo-devs/primo-bg-coding-agent

Length of output: 14477


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Search for rate-limit table names and storage options ==\n'
rg -n 'rate[_-]?limit|ratelimit|_rate_limit|rate_limit' packages/control-plane terraform -S

printf '\n== Inspect browser-auth schema test around declared tables ==\n'
sed -n '1,120p' packages/control-plane/test/integration/browser-auth-schema.test.ts

printf '\n== Inspect Better Auth migration test setup ==\n'
sed -n '1,120p' packages/control-plane/test/integration/browser-auth.test.ts

Repository: primo-devs/primo-bg-coding-agent

Length of output: 9679


🌐 Web query:

Better Auth rate limit storage database memory migration table Workers D1 docs

💡 Result:

Better Auth supports storing rate limit data in a database (such as Cloudflare D1) as an alternative to the default in-memory storage, which is often unsuitable for serverless environments [1]. To use a database for rate limiting, configure your Better Auth instance as follows: import { betterAuth } from "better-auth"; export const auth = betterAuth({ rateLimit: { storage: "database", // Use "database" instead of default memory modelName: "rateLimit", // Optional, defaults to "rateLimit" }, }); Migration Requirements: 1. When using the built-in Kysely adapter with D1, you can create the necessary rate limit table by running the Better Auth CLI: npx auth@latest migrate [2][1]. 2. If you are using an ORM like Drizzle or Prisma, you must first generate the schema using npx auth@latest generate and then apply it using your ORM's native migration tools [2][1]. 3. For Cloudflare D1 specifically, Better Auth now includes native support, eliminating the need for custom adapter setups [3][4]. Since Better Auth migrations often require database introspection that may be difficult in the Cloudflare runtime, some developers use custom helper scripts to run migrations programmatically if the CLI is not viable in their environment [2][5]. Important Considerations for Cloudflare: - Secondary Storage: If using Cloudflare KV for secondary storage (including rate limiting), note that KV enforces a minimum Time To Live (TTL) of 60 seconds [6][7]. You must configure your rate limit windows to be at least 60 seconds to prevent errors, and you may need to override default settings for specific endpoints that default to shorter windows [8][7]. - Schema: If you manage the schema manually, the rateLimit table requires at least a unique identifier for the key, a counter for requests in the current window, and a timestamp for the last request [1].

Citations:


packages/control-plane/src/auth/user/better-auth.ts: use database-backed rate limiting
storage: "memory" only keeps counters per isolate in Workers, so this limit won’t apply consistently across the deployment. Switch to storage: "database" and add the corresponding Better Auth rate-limit migration/table.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/control-plane/src/auth/user/better-auth.ts` around lines 45 - 50,
Update the Better Auth rateLimit configuration to use database-backed storage
instead of memory, and add the corresponding Better Auth rate-limit
migration/table required by the database adapter. Preserve the existing enabled,
window, and max settings.

Comment on lines +66 to +76
socialProviders: {
...(config.github
? {
github: {
...config.github,
disableDefaultScope: true,
},
}
: {}),
...(config.google ? { google: config.google } : {}),
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

GitHub OAuth needs explicit scopes here.

disableDefaultScope: true removes Better Auth’s default GitHub scopes, and config.github doesn’t add a replacement scope array. That leaves the authorization request without read:user and user:email, so GitHub sign-in won’t be able to resolve the user’s profile/email.

🐛 Proposed fix
             github: {
               ...config.github,
               disableDefaultScope: true,
+              scope: ["read:user", "user:email"],
             },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
socialProviders: {
...(config.github
? {
github: {
...config.github,
disableDefaultScope: true,
},
}
: {}),
...(config.google ? { google: config.google } : {}),
},
socialProviders: {
...(config.github
? {
github: {
...config.github,
disableDefaultScope: true,
scope: ["read:user", "user:email"],
},
}
: {}),
...(config.google ? { google: config.google } : {}),
},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/control-plane/src/auth/user/better-auth.ts` around lines 66 - 76,
Update the GitHub provider configuration in the socialProviders setup to retain
disableDefaultScope: true while explicitly supplying scopes for read:user and
user:email. Preserve any values from config.github and ensure the explicit scope
array is applied so GitHub sign-in can resolve the user profile and email.

Comment on lines +1 to +19
import { readdirSync, readFileSync } from "node:fs";
import { DatabaseSync } from "node:sqlite";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";

const MIGRATIONS_DIRECTORY = fileURLToPath(
new URL("../../../../terraform/d1/migrations/", import.meta.url)
);
const BACKFILL_MIGRATION = "0049_backfill_better_auth_accounts.sql";

function applyMigrationsBeforeBackfill(db: DatabaseSync): void {
const migrationFiles = readdirSync(MIGRATIONS_DIRECTORY)
.filter((file) => /^\d{4}_.+\.sql$/.test(file) && file < BACKFILL_MIGRATION)
.sort();

for (const migrationFile of migrationFiles) {
db.exec(readFileSync(`${MIGRATIONS_DIRECTORY}/${migrationFile}`, "utf8"));
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -H -t f -g '.nvmrc' -g '.tool-versions' .
fd -t f 'package.json' -d 3 --exec jq -r '{path: input_filename, engines, volta}' {} \;
rg -n 'node-version|setup-node' -C2 .github/workflows/ci.yml

Repository: primo-devs/primo-bg-coding-agent

Length of output: 2047


🌐 Web query:

Node.js node:sqlite added in which version DatabaseSync availability release notes

💡 Result:

The DatabaseSync class, along with the node:sqlite module, was added in Node.js version 22.5.0 [1][2]. Upon its initial introduction, the module was experimental and required the --experimental-sqlite command-line flag to be enabled [3][4]. In subsequent releases, the module's experimental status was updated: - Starting in Node.js v23.4.0 and v22.13.0, the node:sqlite module was no longer required to be behind the --experimental-sqlite flag, though it remained classified as experimental [1][5]. - As of later versions, the module has reached a release candidate stability status [1][6].

Citations:


Raise the Node floor for this test
node:sqlite is only available from Node 22.5.0, but the repo still allows Node 22.0.0. Tighten the supported version to >=22.5.0 so this import doesn’t fail in a compliant environment.

🧰 Tools
🪛 OpenGrep (1.25.0)

[ERROR] 17-17: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/control-plane/src/db/browser-auth-legacy-migration.test.ts` around
lines 1 - 19, Update the repository’s Node.js version constraint to require
>=22.5.0, locating the package/toolchain configuration that defines the
supported Node 22 range. Keep the existing version policy unchanged apart from
raising the minimum needed by the node:sqlite import in this test.

Comment on lines +66 to +79
const browserGitHubAccountInfoSchema = z.object({
user: z.object({
id: z.string().min(1),
}),
data: z.object({
provider: z.literal("github"),
issuer: z.literal("https://github.com"),
subject: z.string().min(1),
login: githubLoginSchema,
displayName: z.string().min(1).optional(),
verifiedEmails: z.array(z.string()),
primaryEmail: z.string().nullable(),
}),
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

better-auth 1.6 accountInfo API response shape user.id data.subject issuer

💡 Result:

In Better Auth v1.6, the accountInfo API endpoint is designed to return information about a user's linked provider account [1][2][3]. The response structure is an object containing two primary properties: user and data [1]. The response shape is defined as follows: { "user": { "id": "string", "name": "string", "email": "string", "image": "string", "emailVerified": "boolean" }, "data": { // This object contains the raw profile information returned by the provider [1][4] } } Key observations regarding your query: 1. User ID: The user.id field in the response refers to the internal Better Auth unique identifier for the user record, not the provider-specific identifier [1]. 2. Subject and Issuer: Better Auth v1.6 typically represents account identity through provider-specific identifiers (such as sub or id from an OIDC provider) [5][6]. While OIDC standardizes the sub (subject) claim, the Better Auth accountInfo API does not explicitly wrap these into a top-level user.subject or user.issuer field in the standard schema [1]. Instead, the provider-specific profile data (which includes fields like sub) is returned within the data object [1][4]. 3. Evolution: In later versions (and as part of the architecture shift toward v1.7), the system has moved toward more explicit account scoping using issuer and provider-specific account identifiers [7][8], but the core structure of accountInfo in v1.6 remains focused on the user object and the provider-supplied raw data map [1]. For server-side usage in v1.6, you can call the endpoint using the auth.api object, providing an optional userId in the query to identify the target user if you are operating outside of a standard session context [2][3][4]. top_results: [1][2][3][4][7]

Citations:


accountInfo.user.id is not the provider subject. Comparing it to account.subject will break browser enrichment; match on data.subject instead, and keep user.id only for the Better Auth user record.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/control-plane/src/session/identity.ts` around lines 66 - 79, Update
browser GitHub account matching to compare account.subject with
browserGitHubAccountInfoSchema’s data.subject, not user.id; retain user.id
exclusively as the Better Auth user record identifier.

Comment on lines +14 to +21
execFileSync("npm", ["run", "build", "-w", "@open-inspect/shared"], {
cwd: repositoryDirectory,
stdio: "pipe",
});
execFileSync("npm", ["run", "build"], {
cwd: packageDirectory,
stdio: "pipe",
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

git ls-files packages/control-plane/src/worker-build.test.ts packages/control-plane/src | sed -n '1,120p'
echo '---'
wc -l packages/control-plane/src/worker-build.test.ts
echo '---'
cat -n packages/control-plane/src/worker-build.test.ts
echo '---'
rg -n "WORKER_BUILD_TIMEOUT_MS|timeout:" packages/control-plane/src packages/shared/src packages/control-plane -g '!**/dist/**' -g '!**/build/**'
echo '---'
node - <<'JS'
const { execFileSync } = require('node:child_process');
console.log(String(execFileSync).includes('timeout'));
JS

Repository: primo-devs/primo-bg-coding-agent

Length of output: 13444


Bound each synchronous build subprocess. execFileSync ignores Vitest’s test timeout while it blocks the event loop, so a hung npm run build can stall the job indefinitely. Pass timeout: WORKER_BUILD_TIMEOUT_MS to both calls.

🧰 Tools
🪛 ast-grep (0.44.1)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execFileSync } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/control-plane/src/worker-build.test.ts` around lines 14 - 21, Add
timeout: WORKER_BUILD_TIMEOUT_MS to both execFileSync calls in the worker build
test, covering the shared-package and worker-package npm builds while preserving
their existing cwd and stdio options.

Comment on lines +55 to +60
function trustedClientIp(request: Request): string | null {
if (process.env.VERCEL === "1") {
return request.headers.get("X-Vercel-Forwarded-For");
}
return "cf" in request ? request.headers.get("CF-Connecting-IP") : null;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does the Next.js Request object passed to a Route Handler retain the Cloudflare cfproperty when deployed via@opennextjs/cloudflare?

💡 Result:

When using @opennextjs/cloudflare, the standard Next.js Request object passed to a Route Handler does not automatically retain the Cloudflare-specific cf property directly on the request object [1]. Instead, to access Cloudflare-specific metadata and context, you must use the getCloudflareContext utility provided by the @opennextjs/cloudflare package [1]. This function returns an object containing the cf property (which holds the request's Cloudflare metadata), the environment bindings (env), and the execution context (ctx) [2][1]. To use it in a Route Handler, import the utility and call it within your function [1]: import { getCloudflareContext } from "@opennextjs/cloudflare"; export async function GET(request: Request) { const { env, cf, ctx } = getCloudflareContext; // You can now access Cloudflare metadata via the cf object console.log(cf?.country); //... } This approach is necessary because @opennextjs/cloudflare runs Next.js applications in a manner that transforms the request handling, requiring developers to explicitly retrieve the Cloudflare context through the provided adapter utility rather than relying on properties directly attached to the standard Web Request object [1][3].

Citations:


Read Cloudflare metadata from getCloudflareContext()
The OpenNext route-handler Request does not expose Cloudflare's cf metadata, so this branch falls back to null in production and CF-Connecting-IP is never trusted on Cloudflare deployments.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/lib/browser-auth-proxy.ts` around lines 55 - 60, Update
trustedClientIp to obtain Cloudflare metadata through getCloudflareContext()
instead of checking for a cf property on the Request. Preserve the Vercel
X-Vercel-Forwarded-For behavior, and read CF-Connecting-IP from the Cloudflare
context for non-Vercel deployments.

Comment on lines +90 to +106
const timeoutSignal = AbortSignal.timeout(CONTROL_PLANE_FETCH_TIMEOUT_MS);
const signal = fetchOptions.signal
? AbortSignal.any([fetchOptions.signal, timeoutSignal])
: timeoutSignal;
const boundedFetchOptions = {
...fetchOptions,
signal,
};

// On Cloudflare Workers, use the service binding to call the control plane
const binding = await getServiceBinding(correlationFields);
if (binding) {
return binding.fetch(url, fetchOptions);
return binding.fetch(url, boundedFetchOptions);
}

// Fallback: direct fetch (works on Vercel / local dev)
return fetch(url, fetchOptions);
}

/**
* Make a control-plane request signed with web's own sig1 service
* credential — never a user token.
*
* Reserved for the token endpoints (exchange/refresh): issuance must be
* reachable only through web's per-service identity. Throws when
* SERVICE_AUTH_SECRET is not configured; callers treat that as an exchange
* failure.
*/
/**
* Token calls sit on the sign-in path and the background refresh check — an
* unresponsive control plane must fail fast into the callers' existing
* exchange_fallback/request_failed paths, not hang until the platform's own
* timeout.
*/
const SERVICE_FETCH_TIMEOUT_MS = 10_000;

export async function controlPlaneTokenFetch(
path: string,
init: { method: string; body?: string }
): Promise<Response> {
if (
init.method !== "POST" ||
(path !== "/auth/tokens/exchange" && path !== "/auth/tokens/refresh")
) {
throw new Error("Service authentication is restricted to token endpoints");
}
const secret = process.env.SERVICE_AUTH_SECRET;
if (!secret) {
throw new Error("SERVICE_AUTH_SECRET not configured");
}

const normalizedPath = path.startsWith("/") ? path : `/${path}`;
const correlation = await getRequestCorrelation();
const correlationFields = getCorrelationLogFields(correlation);
// The signature covers method, path, query, and body hash — not the host —
// so signing the URL-based form stays valid across the service binding.
const url = `${getControlPlaneUrl()}${normalizedPath}`;

const headers = {
"Content-Type": "application/json",
...(await buildServiceAuthHeaders({
service: "web",
secret,
method: init.method,
url,
body: init.body,
traceId: correlation.traceId,
})),
};

return dispatchControlPlaneFetch(
url,
{
method: init.method,
headers,
body: init.body,
signal: AbortSignal.timeout(SERVICE_FETCH_TIMEOUT_MS),
},
correlationFields
);
return fetch(url, boundedFetchOptions);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Timeout now also bounds streamed response bodies.

AbortSignal.timeout stays armed after headers arrive, so the attachment/media/diff proxies (which return upstream.body as a stream) will have large downloads aborted mid-stream at CONTROL_PLANE_FETCH_TIMEOUT_MS. If streamed passthrough is expected for multi-MB artifacts, consider letting streaming callers opt out (e.g. an option to skip the internal timeout) rather than applying it unconditionally.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/lib/control-plane-transport.ts` around lines 90 - 106, The
unconditional timeout in the transport path also aborts streamed response bodies
after headers are received. Update the fetch-options flow around
boundedFetchOptions and the control-plane callers that proxy
attachment/media/diff streams so streaming requests can explicitly skip the
internal CONTROL_PLANE_FETCH_TIMEOUT_MS signal, while retaining the timeout by
default for normal requests and preserving caller-provided signal handling.

Comment on lines +28 to 47
export async function getServerAuthSession(): Promise<ServerAuthSession | null> {
const cookieStore = await cookies();
const cookieHeader = serializeBrowserSessionCookies(cookieStore.getAll());
if (!cookieHeader) return null;
const response = await dispatchBrowserAuthRequest({
method: "GET",
pathname: "/api/auth/get-session",
headers: { Cookie: cookieHeader },
});

if (response.status === 401) return null;
if (!response.ok) {
throw new Error(`Browser authentication failed with status ${response.status}`);
}

const payload: unknown = await response.json();
if (payload === null) return null;
const session = browserAuthSessionResponseSchema.parse(payload);
return { user: session.user };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

getServerAuthSession can throw for cases its own callers don't catch.

Cookie parsing errors (serializeBrowserSessionCookies duplicate/invalid-value throws), the explicit non-401 status throw (line 40), and browserAuthSessionResponseSchema.parse failures all propagate out of this function. Every consumer shown in the graph evidence (sessions/route.ts GET/POST, automations/route.ts GET/POST, ws-token/route.ts POST, diff/retry, attachments, media routes) calls await getServerAuthSession() before its try/catch block, so any of these throws becomes an unhandled 500 instead of the intended graceful 401/error response. This is new exposure versus the prior NextAuth-backed implementation, which didn't synchronously throw on cookie shape issues.

Consider catching the internal parse/serialize errors here and mapping them to null (unauthenticated) or a well-defined thrown error type that callers are updated to catch, so route handlers keep returning controlled JSON responses.

🛡️ Sketch of a safer wrapper
 export async function getServerAuthSession(): Promise<ServerAuthSession | null> {
   const cookieStore = await cookies();
-  const cookieHeader = serializeBrowserSessionCookies(cookieStore.getAll());
+  let cookieHeader: string | null;
+  try {
+    cookieHeader = serializeBrowserSessionCookies(cookieStore.getAll());
+  } catch {
+    // Malformed/duplicate browser session cookies are treated as unauthenticated.
+    return null;
+  }
   if (!cookieHeader) return null;
   const response = await dispatchBrowserAuthRequest({
     method: "GET",
     pathname: "/api/auth/get-session",
     headers: { Cookie: cookieHeader },
   });

   if (response.status === 401) return null;
   if (!response.ok) {
     throw new Error(`Browser authentication failed with status ${response.status}`);
   }

   const payload: unknown = await response.json();
   if (payload === null) return null;
-  const session = browserAuthSessionResponseSchema.parse(payload);
-  return { user: session.user };
+  const parsed = browserAuthSessionResponseSchema.safeParse(payload);
+  if (!parsed.success) {
+    throw new Error("Browser session response failed validation");
+  }
+  return { user: parsed.data.user };
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/lib/server-auth-session.ts` around lines 28 - 47, Update
getServerAuthSession to contain failures from serializeBrowserSessionCookies,
the non-success dispatchBrowserAuthRequest response, and
browserAuthSessionResponseSchema.parse so callers receive the intended
unauthenticated result or a consistently handled authentication error instead of
an unhandled exception. Prefer returning null for malformed cookies and invalid
session payloads, and ensure any deliberately thrown error is handled
consistently by all consumers.

Comment on lines +21 to +46
INSERT INTO auth_users (
id,
name,
email,
emailVerified,
image,
createdAt,
updatedAt
)
SELECT
users.id,
coalesce(nullif(trim(users.display_name), ''), lower(trim(users.email))),
lower(trim(users.email)),
0,
users.avatar_url,
strftime('%Y-%m-%dT%H:%M:%fZ', users.created_at / 1000.0, 'unixepoch'),
strftime('%Y-%m-%dT%H:%M:%fZ', users.updated_at / 1000.0, 'unixepoch')
FROM users
WHERE users.email IS NOT NULL
AND length(trim(users.email)) > 0
AND NOT EXISTS (
SELECT 1
FROM auth_users
WHERE auth_users.id = users.id
AND lower(trim(auth_users.email)) = lower(trim(users.email))
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the migration and any schema definitions for auth_users / users.
git ls-files | rg 'terraform/d1/migrations/0049_backfill_better_auth_accounts.sql|auth_users|users|better_auth|schema|migrations'

# Show the relevant migration section with line numbers.
sed -n '1,140p' terraform/d1/migrations/0049_backfill_better_auth_accounts.sql

# Search for auth_users table definition and related constraints.
rg -n -A6 -B6 'CREATE TABLE\s+auth_users|auth_users\s*\(' terraform . -g '*.sql' -g '*.ts' -g '*.js' -g '*.json' -g '*.md'

# Search for any code that updates or syncs users.email into auth_users.email.
rg -n 'auth_users.*email|users.*email|lower\(trim\(users\.email\)\)|lower\(trim\(auth_users\.email\)\)' terraform . -g '*.sql' -g '*.ts' -g '*.js'

Repository: primo-devs/primo-bg-coding-agent

Length of output: 23649


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the user-store write paths that might change users.email.
sed -n '120,220p' packages/control-plane/src/db/user-store.ts
sed -n '320,380p' packages/control-plane/src/db/user-store.ts

# Inspect browser-auth callback / projection code for any auth_users sync on user updates.
sed -n '220,340p' packages/control-plane/test/integration/browser-auth-callback.test.ts
sed -n '1,220p' packages/control-plane/src/db/browser-auth-legacy-migration.test.ts

# Search for UPDATEs to users and auth_users across the repo.
rg -n 'UPDATE\s+users\b|UPDATE\s+auth_users\b|INSERT INTO auth_users|DELETE FROM auth_users|set\s+email\s*=' packages terraform -g '*.ts' -g '*.js' -g '*.sql'

# Check whether any code explicitly keeps auth_users.email in sync with users.email.
rg -n 'auth_users.*users\.email|users\.email.*auth_users|sync.*auth_users|projection' packages terraform -g '*.ts' -g '*.js' -g '*.sql'

Repository: primo-devs/primo-bg-coding-agent

Length of output: 19474


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the full user update path for email mutations.
sed -n '220,300p' packages/control-plane/src/db/user-store.ts

# Inspect the canonical user projection helpers for any auth_users sync behavior.
sed -n '1,220p' packages/control-plane/src/auth/user/canonical-user-projection.ts
sed -n '1,240p' packages/control-plane/src/auth/user/better-auth.ts

# Look for any API or service that updates a user's email independently of auth_users.
rg -n 'updateUser\(|set.*email|email.*update|users\.email|auth_users\.email' packages/control-plane/src packages/control-plane/test -g '*.ts'

Repository: primo-devs/primo-bg-coding-agent

Length of output: 10628


Guard the backfill against existing same-id auth_users rows

NOT EXISTS only skips rows when both id and email match. If users.email has changed since auth_users was created for the same id, this insert will hit the primary key and abort the migration. Add ON CONFLICT(id) DO NOTHING here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@terraform/d1/migrations/0049_backfill_better_auth_accounts.sql` around lines
21 - 46, Add an ON CONFLICT(id) DO NOTHING clause to the INSERT into auth_users
in the backfill migration, preserving the existing SELECT and NOT EXISTS
filtering while preventing duplicate primary-key rows from aborting the
migration.

Comment on lines +88 to +103
hashes = [
"h1:OO+IuvQJSPmWdN8AyyIEvPJbLvDQpgX/zbktoa9KsJE=",
"zh:161ad0bd9a75768c82f53fb6e7172a9d8be2d4889b012645a34795031aaf1bf1",
"zh:19dc9a5b17729725ccfc4f45b0500af0ee5bc6b6b160c7adb8f2bf617d2c80ea",
"zh:269eda8fe42daa7974d5a34d166c3ba9defe80cde86c01e4dadcfdf2e1f05e5f",
"zh:373f7c65566f8f2cc7f45d698654feb9d988996957e1266a69ca00c52d6d16d0",
"zh:5599d16804c41c83009ec621b6d6b6f74e102f5827678a4750f8809055546b61",
"zh:583be0440469a22bff70dcfa56593b01566860b29607437264adb51060cf46fc",
"zh:5f211d8ec3f2e1f414870d9584bfe26e6995560ef81c748f8447a48164767398",
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
"zh:7b547fd16216761ef86efc3ed516ac5ac0c5c42b7c7eb24a08cef2d93f69ed5e",
"zh:7e7c0679daf2a382151d05068c8c3f0dae6b7b7dccf818827b73dd08638df2ef",
"zh:8089dec888a8038b9b4fb23b3df7e1057293dbc5b60b42cc47ff690d69d4b61b",
"zh:c51f15a031edfd6f23ce8ced3446ca7f8d8d647e2499890d7d5d10d5016d7257",
"zh:c94784f005708890dc6895afd53636ec00ec1e430b15d41e5aebfb1d4b39bd04",
]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Lock entry has only one h1: hash while neighboring providers have two.

hashicorp/random was likely locked on a single platform. terraform init on a different OS/arch will fail checksum verification. Regenerate with terraform providers lock -platform=linux_amd64 -platform=darwin_arm64 … to match the coverage of the null and vercel entries.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@terraform/environments/production/.terraform.lock.hcl` around lines 88 - 103,
Update the hashicorp/random lock entry’s hashes list to include the complete
platform coverage for linux_amd64 and darwin_arm64, regenerating it with
terraform providers lock and preserving the existing checksums alongside the
additional h1 hash.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants